diff options
Diffstat (limited to 'dom/streams')
66 files changed, 16400 insertions, 0 deletions
diff --git a/dom/streams/BaseQueuingStrategy.h b/dom/streams/BaseQueuingStrategy.h new file mode 100644 index 0000000000..eef7147c08 --- /dev/null +++ b/dom/streams/BaseQueuingStrategy.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_BaseQueuingStrategy_h +#define mozilla_dom_BaseQueuingStrategy_h + +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +class BaseQueuingStrategy : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(BaseQueuingStrategy) + + BaseQueuingStrategy(nsISupports* aGlobal, double aHighWaterMark) + : mGlobal(do_QueryInterface(aGlobal)), mHighWaterMark(aHighWaterMark) {} + + nsIGlobalObject* GetParentObject() const; + + double HighWaterMark() const { return mHighWaterMark; } + + protected: + virtual ~BaseQueuingStrategy() = default; + + protected: + nsCOMPtr<nsIGlobalObject> mGlobal; + double mHighWaterMark = 0.0; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ByteLengthQueuingStrategy.cpp b/dom/streams/ByteLengthQueuingStrategy.cpp new file mode 100644 index 0000000000..858f6d664b --- /dev/null +++ b/dom/streams/ByteLengthQueuingStrategy.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ByteLengthQueuingStrategy.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" + +#include "js/TypeDecls.h" +#include "js/PropertyAndElement.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(ByteLengthQueuingStrategy, + BaseQueuingStrategy) +NS_IMPL_ADDREF_INHERITED(ByteLengthQueuingStrategy, BaseQueuingStrategy) +NS_IMPL_RELEASE_INHERITED(ByteLengthQueuingStrategy, BaseQueuingStrategy) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ByteLengthQueuingStrategy) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(BaseQueuingStrategy) + +/* static */ +already_AddRefed<ByteLengthQueuingStrategy> +ByteLengthQueuingStrategy::Constructor(const GlobalObject& aGlobal, + const QueuingStrategyInit& aInit) { + RefPtr<ByteLengthQueuingStrategy> strategy = new ByteLengthQueuingStrategy( + aGlobal.GetAsSupports(), aInit.mHighWaterMark); + return strategy.forget(); +} + +JSObject* ByteLengthQueuingStrategy::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ByteLengthQueuingStrategy_Binding::Wrap(aCx, this, aGivenProto); +} + +static bool ByteLengthQueuingStrategySize(JSContext* cx, unsigned argc, + JS::Value* vp) { + // https://streams.spec.whatwg.org/#blqs-internal-slots + JS::CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1: Return ? GetV(chunk, "byteLength"). + JS::Rooted<JSObject*> chunkObj(cx, JS::ToObject(cx, args.get(0))); + if (!chunkObj) { + return false; + } + + return JS_GetProperty(cx, chunkObj, "byteLength", args.rval()); +} + +// https://streams.spec.whatwg.org/#blqs-size +already_AddRefed<Function> ByteLengthQueuingStrategy::GetSize( + ErrorResult& aRv) { + // Step 1. Return this's relevant global object's ByteLength queuing strategy + // size function. + if (RefPtr<Function> fun = + mGlobal->GetByteLengthQueuingStrategySizeFunction()) { + return fun.forget(); + } + + // Note: Instead of eagerly allocating a size function for every global object + // we do it lazily once in this getter. + // After this point the steps refer to: + // https://streams.spec.whatwg.org/#byte-length-queuing-strategy-size-function + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + aRv.ThrowUnknownError("Internal error"); + return nullptr; + } + JSContext* cx = jsapi.cx(); + + // Step 1. Let steps be the following steps, given chunk + // Note: See ByteLengthQueuingStrategySize instead. + + // Step 2. Let F be !CreateBuiltinFunction(steps, 1, "size", « », + // globalObject’s relevant Realm). + JS::Rooted<JSFunction*> sizeFunction( + cx, JS_NewFunction(cx, ByteLengthQueuingStrategySize, 1, 0, "size")); + if (!sizeFunction) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + + // Step 3. Set globalObject’s byte length queuing strategy size function to + // a Function that represents a reference to F, + // with callback context equal to globalObject’s relevant settings object. + JS::Rooted<JSObject*> funObj(cx, JS_GetFunctionObject(sizeFunction)); + JS::Rooted<JSObject*> global(cx, mGlobal->GetGlobalJSObject()); + RefPtr<Function> function = new Function(cx, funObj, global, mGlobal); + mGlobal->SetByteLengthQueuingStrategySizeFunction(function); + + return function.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/streams/ByteLengthQueuingStrategy.h b/dom/streams/ByteLengthQueuingStrategy.h new file mode 100644 index 0000000000..64fe3e4528 --- /dev/null +++ b/dom/streams/ByteLengthQueuingStrategy.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ByteLengthQueuingStrategy_h +#define mozilla_dom_ByteLengthQueuingStrategy_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BaseQueuingStrategy.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { +class ByteLengthQueuingStrategy final : public BaseQueuingStrategy, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_INHERITED( + ByteLengthQueuingStrategy, BaseQueuingStrategy) + + public: + explicit ByteLengthQueuingStrategy(nsISupports* aGlobal, + double aHighWaterMark) + : BaseQueuingStrategy(aGlobal, aHighWaterMark) {} + + protected: + ~ByteLengthQueuingStrategy() override = default; + + public: + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<ByteLengthQueuingStrategy> Constructor( + const GlobalObject& aGlobal, const QueuingStrategyInit& aInit); + + already_AddRefed<Function> GetSize(ErrorResult& aRv); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ByteStreamHelpers.cpp b/dom/streams/ByteStreamHelpers.cpp new file mode 100644 index 0000000000..5022bb2c72 --- /dev/null +++ b/dom/streams/ByteStreamHelpers.cpp @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ByteStreamHelpers.h" +#include "mozilla/dom/ReadableByteStreamController.h" +#include "js/ArrayBuffer.h" +#include "js/RootingAPI.h" +#include "js/experimental/TypedData.h" +#include "mozilla/ErrorResult.h" + +namespace mozilla::dom { + +// https://streams.spec.whatwg.org/#transfer-array-buffer +// As some parts of the specifcation want to use the abrupt completion value, +// this function may leave a pending exception if it returns nullptr. +JSObject* TransferArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aObject) { + MOZ_ASSERT(JS::IsArrayBufferObject(aObject)); + + // Step 1. + MOZ_ASSERT(!JS::IsDetachedArrayBufferObject(aObject)); + + // Step 3 (Reordered) + size_t bufferLength = JS::GetArrayBufferByteLength(aObject); + + // Step 2 (Reordered) + UniquePtr<void, JS::FreePolicy> bufferData{ + JS::StealArrayBufferContents(aCx, aObject)}; + + // Step 4. + if (!JS::DetachArrayBuffer(aCx, aObject)) { + return nullptr; + } + + // Step 5. + return JS::NewArrayBufferWithContents(aCx, bufferLength, + std::move(bufferData)); +} + +// https://streams.spec.whatwg.org/#can-transfer-array-buffer +bool CanTransferArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aObject, + ErrorResult& aRv) { + // Step 1. Assert: Type(O) is Object. (Implicit in types) + // Step 2. Assert: O has an [[ArrayBufferData]] internal slot. + MOZ_ASSERT(JS::IsArrayBufferObject(aObject)); + + // Step 3. If ! IsDetachedBuffer(O) is true, return false. + if (JS::IsDetachedArrayBufferObject(aObject)) { + return false; + } + + // Step 4. If SameValue(O.[[ArrayBufferDetachKey]], undefined) is false, + // return false. + // Step 5. Return true. + // Note: WASM memories are the only buffers that would qualify + // as having an [[ArrayBufferDetachKey]] which is not undefined. + bool hasDefinedArrayBufferDetachKey = false; + if (!JS::HasDefinedArrayBufferDetachKey(aCx, aObject, + &hasDefinedArrayBufferDetachKey)) { + aRv.StealExceptionFromJSContext(aCx); + return false; + } + return !hasDefinedArrayBufferDetachKey; +} + +// https://streams.spec.whatwg.org/#abstract-opdef-cloneasuint8array +JSObject* CloneAsUint8Array(JSContext* aCx, JS::Handle<JSObject*> aObject) { + // Step 1. Assert: Type(O) is Object. Implicit. + // Step 2. Assert: O has an [[ViewedArrayBuffer]] internal slot. + MOZ_ASSERT(JS_IsArrayBufferViewObject(aObject)); + + // Step 3. Assert: !IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is false. + bool isShared; + JS::Rooted<JSObject*> viewedArrayBuffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, aObject, &isShared)); + if (!viewedArrayBuffer) { + return nullptr; + } + MOZ_ASSERT(!JS::IsDetachedArrayBufferObject(viewedArrayBuffer)); + + // Step 4. Let buffer be ?CloneArrayBuffer(O.[[ViewedArrayBuffer]], + // O.[[ByteOffset]], O.[[ByteLength]], %ArrayBuffer%). + size_t byteOffset = JS_GetTypedArrayByteOffset(aObject); + size_t byteLength = JS_GetTypedArrayByteLength(aObject); + JS::Rooted<JSObject*> buffer( + aCx, + JS::ArrayBufferClone(aCx, viewedArrayBuffer, byteOffset, byteLength)); + if (!buffer) { + return nullptr; + } + + // Step 5. Let array be ! Construct(%Uint8Array%, « buffer »). + JS::Rooted<JSObject*> array( + aCx, JS_NewUint8ArrayWithBuffer(aCx, buffer, 0, + static_cast<int64_t>(byteLength))); + if (!array) { + return nullptr; + } + + // Step 6. Return array. + return array; +} + +} // namespace mozilla::dom diff --git a/dom/streams/ByteStreamHelpers.h b/dom/streams/ByteStreamHelpers.h new file mode 100644 index 0000000000..8557c9b16b --- /dev/null +++ b/dom/streams/ByteStreamHelpers.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ByteStreamHelpers_h +#define mozilla_dom_ByteStreamHelpers_h + +#include "js/TypeDecls.h" +#include "mozilla/ErrorResult.h" +#include "UnderlyingSourceCallbackHelpers.h" + +namespace mozilla::dom { + +class ReadableStream; +class BodyStreamHolder; + +// https://streams.spec.whatwg.org/#transfer-array-buffer +// +// As some parts of the specifcation want to use the abrupt completion value, +// this function may leave a pending exception if it returns nullptr. +JSObject* TransferArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aObject); + +bool CanTransferArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aObject, + ErrorResult& aRv); + +// If this returns null, it will leave a pending exception on aCx which +// must be handled by the caller (in the spec this is always the case +// currently). +JSObject* CloneAsUint8Array(JSContext* aCx, JS::Handle<JSObject*> aObject); + +MOZ_CAN_RUN_SCRIPT void +SetUpReadableByteStreamControllerFromBodyStreamUnderlyingSource( + JSContext* aCx, ReadableStream* aStream, + BodyStreamHolder* aUnderlyingSource, ErrorResult& aRv); + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/CountQueuingStrategy.cpp b/dom/streams/CountQueuingStrategy.cpp new file mode 100644 index 0000000000..72f000e604 --- /dev/null +++ b/dom/streams/CountQueuingStrategy.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/CountQueuingStrategy.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION(BaseQueuingStrategy, mGlobal) +NS_IMPL_CYCLE_COLLECTING_ADDREF(BaseQueuingStrategy) +NS_IMPL_CYCLE_COLLECTING_RELEASE(BaseQueuingStrategy) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BaseQueuingStrategy) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(CountQueuingStrategy, + BaseQueuingStrategy) +NS_IMPL_ADDREF_INHERITED(CountQueuingStrategy, BaseQueuingStrategy) +NS_IMPL_RELEASE_INHERITED(CountQueuingStrategy, BaseQueuingStrategy) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CountQueuingStrategy) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(BaseQueuingStrategy) + +/* static */ +already_AddRefed<CountQueuingStrategy> CountQueuingStrategy::Constructor( + const GlobalObject& aGlobal, const QueuingStrategyInit& aInit) { + RefPtr<CountQueuingStrategy> strategy = + new CountQueuingStrategy(aGlobal.GetAsSupports(), aInit.mHighWaterMark); + return strategy.forget(); +} + +nsIGlobalObject* BaseQueuingStrategy::GetParentObject() const { + return mGlobal; +} + +JSObject* CountQueuingStrategy::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return CountQueuingStrategy_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://streams.spec.whatwg.org/#count-queuing-strategy-size-function +static bool CountQueuingStrategySize(JSContext* aCx, unsigned aArgc, + JS::Value* aVp) { + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + + // Step 1.1. Return 1. + args.rval().setInt32(1); + return true; +} + +// https://streams.spec.whatwg.org/#cqs-size +already_AddRefed<Function> CountQueuingStrategy::GetSize(ErrorResult& aRv) { + // Step 1. Return this's relevant global object's count queuing strategy + // size function. + if (RefPtr<Function> fun = mGlobal->GetCountQueuingStrategySizeFunction()) { + return fun.forget(); + } + + // Note: Instead of eagerly allocating a size function for every global object + // we do it lazily once in this getter. + // After this point the steps refer to: + // https://streams.spec.whatwg.org/#count-queuing-strategy-size-function. + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + aRv.ThrowUnknownError("Internal error"); + return nullptr; + } + JSContext* cx = jsapi.cx(); + + // Step 1. Let steps be the following steps: + // Note: See CountQueuingStrategySize instead. + + // Step 2. Let F be + // ! CreateBuiltinFunction(steps, 0, "size", « », + // globalObject’s relevant Realm). + JS::Rooted<JSFunction*> sizeFunction( + cx, JS_NewFunction(cx, CountQueuingStrategySize, 0, 0, "size")); + if (!sizeFunction) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + + // Step 3. Set globalObject’s count queuing strategy size function to + // a Function that represents a reference to F, + // with callback context equal to globalObject’s relevant settings object. + JS::Rooted<JSObject*> funObj(cx, JS_GetFunctionObject(sizeFunction)); + JS::Rooted<JSObject*> global(cx, mGlobal->GetGlobalJSObject()); + RefPtr<Function> function = new Function(cx, funObj, global, mGlobal); + mGlobal->SetCountQueuingStrategySizeFunction(function); + + return function.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/streams/CountQueuingStrategy.h b/dom/streams/CountQueuingStrategy.h new file mode 100644 index 0000000000..bbf109efbc --- /dev/null +++ b/dom/streams/CountQueuingStrategy.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_CountQueuingStrategy_h +#define mozilla_dom_CountQueuingStrategy_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BaseQueuingStrategy.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +class CountQueuingStrategy final : public BaseQueuingStrategy, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_INHERITED(CountQueuingStrategy, + BaseQueuingStrategy) + + public: + explicit CountQueuingStrategy(nsISupports* aGlobal, double aHighWaterMark) + : BaseQueuingStrategy(aGlobal, aHighWaterMark) {} + + protected: + ~CountQueuingStrategy() override = default; + + public: + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<CountQueuingStrategy> Constructor( + const GlobalObject& aGlobal, const QueuingStrategyInit& aInit); + + already_AddRefed<Function> GetSize(ErrorResult& aRv); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CountQueuingStrategy_h diff --git a/dom/streams/QueueWithSizes.h b/dom/streams/QueueWithSizes.h new file mode 100644 index 0000000000..5a5bb49036 --- /dev/null +++ b/dom/streams/QueueWithSizes.h @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_QueueWithSizes_h +#define mozilla_dom_QueueWithSizes_h + +#include <cmath> +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +// Note: Consumers of QueueWithSize need to ensure it is traced. See +// ReadableStreamDefaultController.cpp for an example. + +struct ValueWithSize : LinkedListElement<ValueWithSize> { + ValueWithSize(JS::Handle<JS::Value> aValue, double aSize) + : mValue(aValue), mSize(aSize){}; + + JS::Heap<JS::Value> mValue; + double mSize = 0.0f; +}; + +// This type is a little tricky lifetime wise: Despite the fact that we're +// talking about linked list of VaueWithSize, what is actually stored in the +// list are dynamically allocated ValueWithSize*. As a result, these have to be +// deleted. There are two ways this lifetime is managed: +// +// 1. In DequeueValue we pop the first element into a UniquePtr, so that it is +// correctly cleaned up at destruction time. +// 2. We use an AutoCleanLinkedList which will delete elements when destroyed +// or `clear`ed. +using QueueWithSizes = AutoCleanLinkedList<ValueWithSize>; + +// https://streams.spec.whatwg.org/#is-non-negative-number +inline bool IsNonNegativeNumber(double v) { + // Step 1. Implicit. + // Step 2. + if (std::isnan(v)) { + return false; + } + + // Step 3. + return !(v < 0); +} + +// https://streams.spec.whatwg.org/#enqueue-value-with-size +template <class QueueContainingClass> +inline void EnqueueValueWithSize(QueueContainingClass aContainer, + JS::Handle<JS::Value> aValue, double aSize, + ErrorResult& aRv) { + // Step 1. Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots. (Implicit by template instantiation.) + // Step 2. If ! IsNonNegativeNumber(size) is false, throw a RangeError + // exception. + if (!IsNonNegativeNumber(aSize)) { + aRv.ThrowRangeError("invalid size"); + return; + } + + // Step 3. If size is +∞, throw a RangeError exception. + if (std::isinf(aSize)) { + aRv.ThrowRangeError("Infinite queue size"); + return; + } + + // Step 4. Append a new value-with-size with value value and size size to + // container.[[queue]]. + // (See the comment on QueueWithSizes for the lifetime reasoning around this + // allocation.) + ValueWithSize* valueWithSize = new ValueWithSize(aValue, aSize); + aContainer->Queue().insertBack(valueWithSize); + // Step 5. Set container.[[queueTotalSize]] to container.[[queueTotalSize]] + + // size. + aContainer->SetQueueTotalSize(aContainer->QueueTotalSize() + aSize); +} + +// https://streams.spec.whatwg.org/#dequeue-value +template <class QueueContainingClass> +inline void DequeueValue(QueueContainingClass aContainer, + JS::MutableHandle<JS::Value> aResultValue) { + // Step 1. Implicit via template instantiation. + // Step 2. + MOZ_ASSERT(!aContainer->Queue().isEmpty()); + + // Step 3+4 + // UniquePtr to ensure memory is freed. + UniquePtr<ValueWithSize> valueWithSize(aContainer->Queue().popFirst()); + + // Step 5. + aContainer->SetQueueTotalSize(aContainer->QueueTotalSize() - + valueWithSize->mSize); + + // Step 6. + if (aContainer->QueueTotalSize() < 0) { + aContainer->SetQueueTotalSize(0); + } + + // Step 7. + aResultValue.set(valueWithSize->mValue); +} + +// https://streams.spec.whatwg.org/#peek-queue-value +template <class QueueContainingClass> +inline void PeekQueueValue(QueueContainingClass aContainer, + JS::MutableHandle<JS::Value> aResultValue) { + // Step 1. Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots. + // Step 2. Assert: container.[[queue]] is not empty. + MOZ_ASSERT(!aContainer->Queue().isEmpty()); + + // Step 3. Let valueWithSize be container.[[queue]][0]. + ValueWithSize* valueWithSize = aContainer->Queue().getFirst(); + + // Step 4. Return valueWithSize’s value. + aResultValue.set(valueWithSize->mValue); +} + +// https://streams.spec.whatwg.org/#reset-queue +template <class QueueContainingClass> +inline void ResetQueue(QueueContainingClass aContainer) { + // Step 1. Assert: container has [[queue]] and [[queueTotalSize]] internal + // slots. (implicit) + + // Step 2. Set container.[[queue]] to a new empty list. + aContainer->Queue().clear(); + + // Step 3. Set container.[[queueTotalSize]] to 0. + aContainer->SetQueueTotalSize(0.0); +} + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadIntoRequest.h b/dom/streams/ReadIntoRequest.h new file mode 100644 index 0000000000..9be3d2c027 --- /dev/null +++ b/dom/streams/ReadIntoRequest.h @@ -0,0 +1,48 @@ + +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadIntoRequest_h +#define mozilla_dom_ReadIntoRequest_h + +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/LinkedList.h" +#include "mozilla/RefPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +// This list element needs to be cycle collected by owning structures. +struct ReadIntoRequest : public nsISupports, + public LinkedListElement<RefPtr<ReadIntoRequest>> { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ReadIntoRequest) + + // An algorithm taking a chunk, called when a chunk is available for reading + virtual void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) = 0; + + // An algorithm taking a chunk or undefined, called when no chunks are + // available because the stream is closed + MOZ_CAN_RUN_SCRIPT + virtual void CloseSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) = 0; + + // An algorithm taking a JavaScript value, called when no chunks are available + // because the stream is errored + virtual void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e, + ErrorResult& aRv) = 0; + + protected: + virtual ~ReadIntoRequest() = default; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadRequest.h b/dom/streams/ReadRequest.h new file mode 100644 index 0000000000..8de991b4b9 --- /dev/null +++ b/dom/streams/ReadRequest.h @@ -0,0 +1,42 @@ + +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadRequest_h +#define mozilla_dom_ReadRequest_h + +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/LinkedList.h" +#include "mozilla/RefPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +// List Owners must traverse this class. +struct ReadRequest : public nsISupports, + public LinkedListElement<RefPtr<ReadRequest>> { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ReadRequest) + + // PipeToReadRequest::ChunkSteps can run script, for example. + MOZ_CAN_RUN_SCRIPT virtual void ChunkSteps(JSContext* aCx, + JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) = 0; + MOZ_CAN_RUN_SCRIPT virtual void CloseSteps(JSContext* aCx, + ErrorResult& aRv) = 0; + virtual void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e, + ErrorResult& aRv) = 0; + + protected: + virtual ~ReadRequest() = default; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadableByteStreamController.cpp b/dom/streams/ReadableByteStreamController.cpp new file mode 100644 index 0000000000..1ce2ae4331 --- /dev/null +++ b/dom/streams/ReadableByteStreamController.cpp @@ -0,0 +1,2087 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ReadableByteStreamController.h" + +#include "ReadIntoRequest.h" +#include "js/ArrayBuffer.h" +#include "js/ErrorReport.h" +#include "js/Exception.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "js/ValueArray.h" +#include "js/experimental/TypedData.h" +#include "js/friend/ErrorMessages.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/ByteStreamHelpers.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/ReadableByteStreamControllerBinding.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamBYOBReader.h" +#include "mozilla/dom/ReadableStreamBYOBRequest.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/ReadableStreamGenericReader.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" + +#include <algorithm> // std::min + +namespace mozilla::dom { + +using namespace streams_abstract; + +// https://streams.spec.whatwg.org/#readable-byte-stream-queue-entry +struct ReadableByteStreamQueueEntry + : LinkedListElement<RefPtr<ReadableByteStreamQueueEntry>> { + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING( + ReadableByteStreamQueueEntry) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS( + ReadableByteStreamQueueEntry) + + ReadableByteStreamQueueEntry(JS::Handle<JSObject*> aBuffer, + size_t aByteOffset, size_t aByteLength) + : mBuffer(aBuffer), mByteOffset(aByteOffset), mByteLength(aByteLength) { + mozilla::HoldJSObjects(this); + } + + JSObject* Buffer() const { return mBuffer; } + void SetBuffer(JS::Handle<JSObject*> aBuffer) { mBuffer = aBuffer; } + + size_t ByteOffset() const { return mByteOffset; } + void SetByteOffset(size_t aByteOffset) { mByteOffset = aByteOffset; } + + size_t ByteLength() const { return mByteLength; } + void SetByteLength(size_t aByteLength) { mByteLength = aByteLength; } + + private: + // An ArrayBuffer, which will be a transferred version of the one originally + // supplied by the underlying byte source. + JS::Heap<JSObject*> mBuffer; + + // A nonnegative integer number giving the byte offset derived from the view + // originally supplied by the underlying byte source + size_t mByteOffset = 0; + + // A nonnegative integer number giving the byte length derived from the view + // originally supplied by the underlying byte source + size_t mByteLength = 0; + + ~ReadableByteStreamQueueEntry() { mozilla::DropJSObjects(this); } +}; + +NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(ReadableByteStreamQueueEntry, (), + (mBuffer)); + +struct PullIntoDescriptor final + : LinkedListElement<RefPtr<PullIntoDescriptor>> { + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PullIntoDescriptor) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(PullIntoDescriptor) + + enum Constructor { + DataView, +#define DEFINE_TYPED_CONSTRUCTOR_ENUM_NAMES(ExternalT, NativeT, Name) Name, + JS_FOR_EACH_TYPED_ARRAY(DEFINE_TYPED_CONSTRUCTOR_ENUM_NAMES) +#undef DEFINE_TYPED_CONSTRUCTOR_ENUM_NAMES + }; + + static Constructor constructorFromScalar(JS::Scalar::Type type) { + switch (type) { +#define REMAP_PULL_INTO_DESCRIPTOR_TYPE(ExternalT, NativeT, Name) \ + case JS::Scalar::Name: \ + return Constructor::Name; + JS_FOR_EACH_TYPED_ARRAY(REMAP_PULL_INTO_DESCRIPTOR_TYPE) +#undef REMAP + + case JS::Scalar::Int64: + case JS::Scalar::Simd128: + case JS::Scalar::MaxTypedArrayViewType: + break; + } + MOZ_CRASH("Unexpected Scalar::Type"); + } + + PullIntoDescriptor(JS::Handle<JSObject*> aBuffer, uint64_t aBufferByteLength, + uint64_t aByteOffset, uint64_t aByteLength, + uint64_t aBytesFilled, uint64_t aElementSize, + Constructor aViewConstructor, ReaderType aReaderType) + : mBuffer(aBuffer), + mBufferByteLength(aBufferByteLength), + mByteOffset(aByteOffset), + mByteLength(aByteLength), + mBytesFilled(aBytesFilled), + mElementSize(aElementSize), + mViewConstructor(aViewConstructor), + mReaderType(aReaderType) { + mozilla::HoldJSObjects(this); + } + + JSObject* Buffer() const { return mBuffer; } + void SetBuffer(JS::Handle<JSObject*> aBuffer) { mBuffer = aBuffer; } + + uint64_t BufferByteLength() const { return mBufferByteLength; } + void SetBufferByteLength(const uint64_t aBufferByteLength) { + mBufferByteLength = aBufferByteLength; + } + + uint64_t ByteOffset() const { return mByteOffset; } + void SetByteOffset(const uint64_t aByteOffset) { mByteOffset = aByteOffset; } + + uint64_t ByteLength() const { return mByteLength; } + void SetByteLength(const uint64_t aByteLength) { mByteLength = aByteLength; } + + uint64_t BytesFilled() const { return mBytesFilled; } + void SetBytesFilled(const uint64_t aBytesFilled) { + mBytesFilled = aBytesFilled; + } + + uint64_t ElementSize() const { return mElementSize; } + void SetElementSize(const uint64_t aElementSize) { + mElementSize = aElementSize; + } + + Constructor ViewConstructor() const { return mViewConstructor; } + + // Note: Named GetReaderType to avoid name conflict with type. + ReaderType GetReaderType() const { return mReaderType; } + void SetReaderType(const ReaderType aReaderType) { + mReaderType = aReaderType; + } + + private: + JS::Heap<JSObject*> mBuffer; + uint64_t mBufferByteLength = 0; + uint64_t mByteOffset = 0; + uint64_t mByteLength = 0; + uint64_t mBytesFilled = 0; + uint64_t mElementSize = 0; + Constructor mViewConstructor; + ReaderType mReaderType; + + ~PullIntoDescriptor() { mozilla::DropJSObjects(this); } +}; + +NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(PullIntoDescriptor, (), (mBuffer)); + +NS_IMPL_CYCLE_COLLECTION_CLASS(ReadableByteStreamController) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ReadableByteStreamController, + ReadableStreamController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mByobRequest, mQueue, mPendingPullIntos) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ReadableByteStreamController, + ReadableStreamController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mByobRequest, mQueue, mPendingPullIntos) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ReadableByteStreamController, + ReadableStreamController) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(ReadableByteStreamController, ReadableStreamController) +NS_IMPL_RELEASE_INHERITED(ReadableByteStreamController, + ReadableStreamController) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableByteStreamController) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(ReadableStreamController) + +ReadableByteStreamController::ReadableByteStreamController( + nsIGlobalObject* aGlobal) + : ReadableStreamController(aGlobal) {} + +ReadableByteStreamController::~ReadableByteStreamController() = default; + +void ReadableByteStreamController::ClearQueue() { mQueue.clear(); } + +void ReadableByteStreamController::ClearPendingPullIntos() { + mPendingPullIntos.clear(); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamcontrollergetbyobrequest +already_AddRefed<ReadableStreamBYOBRequest> +ReadableByteStreamControllerGetBYOBRequest( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv) { + // Step 1. + if (!aController->GetByobRequest() && + !aController->PendingPullIntos().isEmpty()) { + // Step 1.1: + PullIntoDescriptor* firstDescriptor = + aController->PendingPullIntos().getFirst(); + + // Step 1.2: + aRv.MightThrowJSException(); + JS::Rooted<JSObject*> buffer(aCx, firstDescriptor->Buffer()); + JS::Rooted<JSObject*> view( + aCx, JS_NewUint8ArrayWithBuffer( + aCx, buffer, + firstDescriptor->ByteOffset() + firstDescriptor->BytesFilled(), + int64_t(firstDescriptor->ByteLength() - + firstDescriptor->BytesFilled()))); + if (!view) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 1.3: + RefPtr<ReadableStreamBYOBRequest> byobRequest = + new ReadableStreamBYOBRequest(aController->GetParentObject()); + + // Step 1.4: + byobRequest->SetController(aController); + + // Step 1.5: + byobRequest->SetView(view); + + // Step 1.6: + aController->SetByobRequest(byobRequest); + } + + // Step 2. + RefPtr<ReadableStreamBYOBRequest> request(aController->GetByobRequest()); + return request.forget(); +} +} // namespace streams_abstract + +already_AddRefed<ReadableStreamBYOBRequest> +ReadableByteStreamController::GetByobRequest(JSContext* aCx, ErrorResult& aRv) { + return ReadableByteStreamControllerGetBYOBRequest(aCx, this, aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-get-desired-size +Nullable<double> ReadableByteStreamControllerGetDesiredSize( + const ReadableByteStreamController* aController) { + // Step 1. + ReadableStream::ReaderState state = aController->Stream()->State(); + + // Step 2. + if (state == ReadableStream::ReaderState::Errored) { + return nullptr; + } + + // Step 3. + if (state == ReadableStream::ReaderState::Closed) { + return 0.0; + } + + // Step 4. + return aController->StrategyHWM() - aController->QueueTotalSize(); +} + +Nullable<double> ReadableByteStreamController::GetDesiredSize() const { + // Step 1. + return ReadableByteStreamControllerGetDesiredSize(this); +} + +JSObject* ReadableByteStreamController::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ReadableByteStreamController_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-invalidate-byob-request +static void ReadableByteStreamControllerInvalidateBYOBRequest( + ReadableByteStreamController* aController) { + // Step 1. + if (!aController->GetByobRequest()) { + return; + } + + // Step 2. + aController->GetByobRequest()->SetController(nullptr); + + // Step 3. + aController->GetByobRequest()->SetView(nullptr); + + // Step 4. + aController->SetByobRequest(nullptr); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-clear-pending-pull-intos +void ReadableByteStreamControllerClearPendingPullIntos( + ReadableByteStreamController* aController) { + // Step 1. + ReadableByteStreamControllerInvalidateBYOBRequest(aController); + + // Step 2. + aController->ClearPendingPullIntos(); +} + +// https://streams.spec.whatwg.org/#reset-queue +void ResetQueue(ReadableByteStreamController* aContainer) { + // Step 1. Implied by type. + // Step 2. + aContainer->ClearQueue(); + + // Step 3. + aContainer->SetQueueTotalSize(0); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-clear-algorithms +void ReadableByteStreamControllerClearAlgorithms( + ReadableByteStreamController* aController) { + // Step 1. Set controller.[[pullAlgorithm]] to undefined. + // Step 2. Set controller.[[cancelAlgorithm]] to undefined. + aController->ClearAlgorithms(); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-error +void ReadableByteStreamControllerError( + ReadableByteStreamController* aController, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + // Step 1. Let stream be controller.[[stream]]. + ReadableStream* stream = aController->Stream(); + + // Step 2. If stream.[[state]] is not "readable", return. + if (stream->State() != ReadableStream::ReaderState::Readable) { + return; + } + + // Step 3. Perform + // !ReadableByteStreamControllerClearPendingPullIntos(controller). + ReadableByteStreamControllerClearPendingPullIntos(aController); + + // Step 4. Perform !ResetQueue(controller). + ResetQueue(aController); + + // Step 5. Perform !ReadableByteStreamControllerClearAlgorithms(controller). + ReadableByteStreamControllerClearAlgorithms(aController); + + // Step 6. Perform !ReadableStreamError(stream, e). + AutoJSAPI jsapi; + if (!jsapi.Init(aController->GetParentObject())) { + return; + } + ReadableStreamError(jsapi.cx(), stream, aValue, aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-close +void ReadableByteStreamControllerClose( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv) { + // Step 1. + RefPtr<ReadableStream> stream = aController->Stream(); + + // Step 2. + if (aController->CloseRequested() || + stream->State() != ReadableStream::ReaderState::Readable) { + return; + } + + // Step 3. + if (aController->QueueTotalSize() > 0) { + // Step 3.1 + aController->SetCloseRequested(true); + // Step 3.2 + return; + } + + // Step 4. + if (!aController->PendingPullIntos().isEmpty()) { + // Step 4.1 + PullIntoDescriptor* firstPendingPullInto = + aController->PendingPullIntos().getFirst(); + // Step 4.2 + if (firstPendingPullInto->BytesFilled() > 0) { + // Step 4.2.1 + ErrorResult rv; + rv.ThrowTypeError("Leftover Bytes"); + + JS::Rooted<JS::Value> exception(aCx); + MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), &exception)); + + // Step 4.2.2 + ReadableByteStreamControllerError(aController, exception, aRv); + if (aRv.Failed()) { + return; + } + + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, exception); + return; + } + } + + // Step 5. + ReadableByteStreamControllerClearAlgorithms(aController); + + // Step 6. + ReadableStreamClose(aCx, stream, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rbs-controller-close +void ReadableByteStreamController::Close(JSContext* aCx, ErrorResult& aRv) { + // Step 1. + if (mCloseRequested) { + aRv.ThrowTypeError("Close already requested"); + return; + } + + // Step 2. + if (Stream()->State() != ReadableStream::ReaderState::Readable) { + aRv.ThrowTypeError("Closing un-readable stream controller"); + return; + } + + // Step 3. + ReadableByteStreamControllerClose(aCx, this, aRv); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-enqueue-chunk-to-queue +void ReadableByteStreamControllerEnqueueChunkToQueue( + ReadableByteStreamController* aController, + JS::Handle<JSObject*> aTransferredBuffer, size_t aByteOffset, + size_t aByteLength) { + // Step 1. + RefPtr<ReadableByteStreamQueueEntry> queueEntry = + new ReadableByteStreamQueueEntry(aTransferredBuffer, aByteOffset, + aByteLength); + aController->Queue().insertBack(queueEntry); + + // Step 2. + aController->AddToQueueTotalSize(double(aByteLength)); +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamcontrollerenqueueclonedchunktoqueue +void ReadableByteStreamControllerEnqueueClonedChunkToQueue( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aBuffer, size_t aByteOffset, size_t aByteLength, + ErrorResult& aRv) { + // Step 1. Let cloneResult be CloneArrayBuffer(buffer, byteOffset, byteLength, + // %ArrayBuffer%). + aRv.MightThrowJSException(); + JS::Rooted<JSObject*> cloneResult( + aCx, JS::ArrayBufferClone(aCx, aBuffer, aByteOffset, aByteLength)); + + // Step 2. If cloneResult is an abrupt completion, + if (!cloneResult) { + JS::Rooted<JS::Value> exception(aCx); + if (!JS_GetPendingException(aCx, &exception)) { + // Uncatchable exception; we should mark aRv and return. + aRv.StealExceptionFromJSContext(aCx); + return; + } + JS_ClearPendingException(aCx); + + // Step 2.1. Perform ! ReadableByteStreamControllerError(controller, + // cloneResult.[[Value]]). + ReadableByteStreamControllerError(aController, exception, aRv); + if (aRv.Failed()) { + return; + } + + // Step 2.2. Return cloneResult. + aRv.ThrowJSException(aCx, exception); + return; + } + + // Step 3. Perform ! + // ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // cloneResult.[[Value]], 0, byteLength). + ReadableByteStreamControllerEnqueueChunkToQueue(aController, cloneResult, 0, + aByteLength); +} + +already_AddRefed<PullIntoDescriptor> +ReadableByteStreamControllerShiftPendingPullInto( + ReadableByteStreamController* aController); + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamcontrollerenqueuedetachedpullintotoqueue +void ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue( + JSContext* aCx, ReadableByteStreamController* aController, + PullIntoDescriptor* aPullIntoDescriptor, ErrorResult& aRv) { + // Step 1. Assert: pullIntoDescriptor’s reader type is "none". + MOZ_ASSERT(aPullIntoDescriptor->GetReaderType() == ReaderType::None); + + // Step 2. If pullIntoDescriptor’s bytes filled > 0, + // perform ? ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, + // pullIntoDescriptor’s buffer, pullIntoDescriptor’s byte offset, + // pullIntoDescriptor’s bytes filled). + if (aPullIntoDescriptor->BytesFilled() > 0) { + JS::Rooted<JSObject*> buffer(aCx, aPullIntoDescriptor->Buffer()); + ReadableByteStreamControllerEnqueueClonedChunkToQueue( + aCx, aController, buffer, aPullIntoDescriptor->ByteOffset(), + aPullIntoDescriptor->BytesFilled(), aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 3. Perform ! + // ReadableByteStreamControllerShiftPendingPullInto(controller). + RefPtr<PullIntoDescriptor> discarded = + ReadableByteStreamControllerShiftPendingPullInto(aController); + (void)discarded; +} + +// https://streams.spec.whatwg.org/#readable-stream-get-num-read-into-requests +static size_t ReadableStreamGetNumReadIntoRequests(ReadableStream* aStream) { + // Step 1. + MOZ_ASSERT(ReadableStreamHasBYOBReader(aStream)); + + // Step 2. + return aStream->GetReader()->AsBYOB()->ReadIntoRequests().length(); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-should-call-pull +bool ReadableByteStreamControllerShouldCallPull( + ReadableByteStreamController* aController) { + // Step 1. Let stream be controller.[[stream]]. + ReadableStream* stream = aController->Stream(); + + // Step 2. If stream.[[state]] is not "readable", return false. + if (stream->State() != ReadableStream::ReaderState::Readable) { + return false; + } + + // Step 3. If controller.[[closeRequested]] is true, return false. + if (aController->CloseRequested()) { + return false; + } + + // Step 4. If controller.[[started]] is false, return false. + if (!aController->Started()) { + return false; + } + + // Step 5. If ! ReadableStreamHasDefaultReader(stream) is true + // and ! ReadableStreamGetNumReadRequests(stream) > 0, return true. + if (ReadableStreamHasDefaultReader(stream) && + ReadableStreamGetNumReadRequests(stream) > 0) { + return true; + } + + // Step 6. If ! ReadableStreamHasBYOBReader(stream) is true + // and ! ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. + if (ReadableStreamHasBYOBReader(stream) && + ReadableStreamGetNumReadIntoRequests(stream) > 0) { + return true; + } + + // Step 7. Let desiredSize be + // ! ReadableByteStreamControllerGetDesiredSize(controller). + Nullable<double> desiredSize = + ReadableByteStreamControllerGetDesiredSize(aController); + + // Step 8. Assert: desiredSize is not null. + MOZ_ASSERT(!desiredSize.IsNull()); + + // Step 9. If desiredSize > 0, return true. + // Step 10. Return false. + return desiredSize.Value() > 0; +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-call-pull-if-needed +void ReadableByteStreamControllerCallPullIfNeeded( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv) { + // Step 1. + bool shouldPull = ReadableByteStreamControllerShouldCallPull(aController); + + // Step 2. + if (!shouldPull) { + return; + } + + // Step 3. + if (aController->Pulling()) { + aController->SetPullAgain(true); + return; + } + + // Step 4. + MOZ_ASSERT(!aController->PullAgain()); + + // Step 5. + aController->SetPulling(true); + + // Step 6. + RefPtr<ReadableStreamController> controller(aController); + RefPtr<UnderlyingSourceAlgorithmsBase> algorithms = + aController->GetAlgorithms(); + RefPtr<Promise> pullPromise = algorithms->PullCallback(aCx, *controller, aRv); + if (aRv.Failed()) { + return; + } + + // Steps 7+8 + pullPromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableByteStreamController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + // Step 7.1 + aController->SetPulling(false); + // Step 7.2 + if (aController->PullAgain()) { + // Step 7.2.1 + aController->SetPullAgain(false); + + // Step 7.2.2 + ReadableByteStreamControllerCallPullIfNeeded( + aCx, MOZ_KnownLive(aController), aRv); + } + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableByteStreamController* aController) { + // Step 8.1 + ReadableByteStreamControllerError(aController, aValue, aRv); + }, + RefPtr(aController)); +} + +bool ReadableByteStreamControllerFillPullIntoDescriptorFromQueue( + JSContext* aCx, ReadableByteStreamController* aController, + PullIntoDescriptor* aPullIntoDescriptor, ErrorResult& aRv); + +JSObject* ReadableByteStreamControllerConvertPullIntoDescriptor( + JSContext* aCx, PullIntoDescriptor* pullIntoDescriptor, ErrorResult& aRv); + +// https://streams.spec.whatwg.org/#readable-stream-fulfill-read-into-request +MOZ_CAN_RUN_SCRIPT +void ReadableStreamFulfillReadIntoRequest(JSContext* aCx, + ReadableStream* aStream, + JS::Handle<JS::Value> aChunk, + bool done, ErrorResult& aRv) { + // Step 1. Assert: !ReadableStreamHasBYOBReader(stream) is true. + MOZ_ASSERT(ReadableStreamHasBYOBReader(aStream)); + + // Step 2. Let reader be stream.[[reader]]. + ReadableStreamBYOBReader* reader = aStream->GetReader()->AsBYOB(); + + // Step 3. Assert: reader.[[readIntoRequests]] is not empty. + MOZ_ASSERT(!reader->ReadIntoRequests().isEmpty()); + + // Step 4. Let readIntoRequest be reader.[[readIntoRequests]][0]. + // Step 5. Remove readIntoRequest from reader.[[readIntoRequests]]. + RefPtr<ReadIntoRequest> readIntoRequest = + reader->ReadIntoRequests().popFirst(); + + // Step 6. If done is true, perform readIntoRequest’s close steps, given + // chunk. + if (done) { + readIntoRequest->CloseSteps(aCx, aChunk, aRv); + return; + } + + // Step 7. Otherwise, perform readIntoRequest’s chunk steps, given chunk. + readIntoRequest->ChunkSteps(aCx, aChunk, aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-commit-pull-into-descriptor +MOZ_CAN_RUN_SCRIPT +void ReadableByteStreamControllerCommitPullIntoDescriptor( + JSContext* aCx, ReadableStream* aStream, + PullIntoDescriptor* pullIntoDescriptor, ErrorResult& aRv) { + // Step 1. Assert: stream.[[state]] is not "errored". + MOZ_ASSERT(aStream->State() != ReadableStream::ReaderState::Errored); + + // Step 2. Assert: pullIntoDescriptor.reader type is not "none". + MOZ_ASSERT(pullIntoDescriptor->GetReaderType() != ReaderType::None); + + // Step 3. Let done be false. + bool done = false; + + // Step 4. If stream.[[state]] is "closed", + if (aStream->State() == ReadableStream::ReaderState::Closed) { + // Step 4.1. Assert: pullIntoDescriptor’s bytes filled is 0. + MOZ_ASSERT(pullIntoDescriptor->BytesFilled() == 0); + + // Step 4.2. Set done to true. + done = true; + } + + // Step 5. Let filledView be ! + // ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). + JS::Rooted<JSObject*> filledView( + aCx, ReadableByteStreamControllerConvertPullIntoDescriptor( + aCx, pullIntoDescriptor, aRv)); + if (aRv.Failed()) { + return; + } + JS::Rooted<JS::Value> filledViewValue(aCx, JS::ObjectValue(*filledView)); + + // Step 6. If pullIntoDescriptor’s reader type is "default", + if (pullIntoDescriptor->GetReaderType() == ReaderType::Default) { + // Step 6.1. Perform !ReadableStreamFulfillReadRequest(stream, filledView, + // done). + ReadableStreamFulfillReadRequest(aCx, aStream, filledViewValue, done, aRv); + return; + } + + // Step 7.1. Assert: pullIntoDescriptor’s reader type is "byob". + MOZ_ASSERT(pullIntoDescriptor->GetReaderType() == ReaderType::BYOB); + + // Step 7.2 Perform !ReadableStreamFulfillReadIntoRequest(stream, filledView, + // done). + ReadableStreamFulfillReadIntoRequest(aCx, aStream, filledViewValue, done, + aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-process-pull-into-descriptors-using-queue +MOZ_CAN_RUN_SCRIPT +void ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv) { + // Step 1. Assert: controller.[[closeRequested]] is false. + MOZ_ASSERT(!aController->CloseRequested()); + + // Step 2. While controller.[[pendingPullIntos]] is not empty, + while (!aController->PendingPullIntos().isEmpty()) { + // Step 2.1 If controller.[[queueTotalSize]] is 0, return. + if (aController->QueueTotalSize() == 0) { + return; + } + + // Step 2.2. Let pullIntoDescriptor be controller.[[pendingPullIntos]][0]. + RefPtr<PullIntoDescriptor> pullIntoDescriptor = + aController->PendingPullIntos().getFirst(); + + // Step 2.3. If + // !ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, + // pullIntoDescriptor) is true, + bool ready = ReadableByteStreamControllerFillPullIntoDescriptorFromQueue( + aCx, aController, pullIntoDescriptor, aRv); + if (aRv.Failed()) { + return; + } + + if (ready) { + // Step 2.3.1. Perform + // !ReadableByteStreamControllerShiftPendingPullInto(controller). + RefPtr<PullIntoDescriptor> discardedPullIntoDescriptor = + ReadableByteStreamControllerShiftPendingPullInto(aController); + + // Step 2.3.2. Perform + // !ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[stream]], + // pullIntoDescriptor). + RefPtr<ReadableStream> stream(aController->Stream()); + ReadableByteStreamControllerCommitPullIntoDescriptor( + aCx, stream, pullIntoDescriptor, aRv); + if (aRv.Failed()) { + return; + } + } + } +} + +MOZ_CAN_RUN_SCRIPT +void ReadableByteStreamControllerHandleQueueDrain( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv); + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamcontrollerfillreadrequestfromqueue +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerFillReadRequestFromQueue( + JSContext* aCx, ReadableByteStreamController* aController, + ReadRequest* aReadRequest, ErrorResult& aRv) { + // Step 1. Assert: controller.[[queueTotalSize]] > 0. + MOZ_ASSERT(aController->QueueTotalSize() > 0); + // Also assert that the queue has a non-zero length; + MOZ_ASSERT(aController->Queue().length() > 0); + + // Step 2. Let entry be controller.[[queue]][0]. + // Step 3. Remove entry from controller.[[queue]]. + RefPtr<ReadableByteStreamQueueEntry> entry = aController->Queue().popFirst(); + + // Assert that we actually got an entry. + MOZ_ASSERT(entry); + + // Step 4. Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] + // − entry’s byte length. + aController->SetQueueTotalSize(aController->QueueTotalSize() - + double(entry->ByteLength())); + + // Step 5. Perform ! ReadableByteStreamControllerHandleQueueDrain(controller). + ReadableByteStreamControllerHandleQueueDrain(aCx, aController, aRv); + if (aRv.Failed()) { + return; + } + + // Step 6. Let view be ! Construct(%Uint8Array%, « entry’s buffer, entry’s + // byte offset, entry’s byte length »). + aRv.MightThrowJSException(); + JS::Rooted<JSObject*> buffer(aCx, entry->Buffer()); + JS::Rooted<JSObject*> view( + aCx, JS_NewUint8ArrayWithBuffer(aCx, buffer, entry->ByteOffset(), + int64_t(entry->ByteLength()))); + if (!view) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // Step 7. Perform readRequest’s chunk steps, given view. + JS::Rooted<JS::Value> viewValue(aCx, JS::ObjectValue(*view)); + aReadRequest->ChunkSteps(aCx, viewValue, aRv); +} + +MOZ_CAN_RUN_SCRIPT void +ReadableByteStreamControllerProcessReadRequestsUsingQueue( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv) { + // Step 1. Let reader be controller.[[stream]].[[reader]]. + // Step 2. Assert: reader implements ReadableStreamDefaultReader. + RefPtr<ReadableStreamDefaultReader> reader = + aController->Stream()->GetDefaultReader(); + + // Step 3. While reader.[[readRequests]] is not empty, + while (!reader->ReadRequests().isEmpty()) { + // Step 3.1. If controller.[[queueTotalSize]] is 0, return. + if (aController->QueueTotalSize() == 0) { + return; + } + + // Step 3.2. Let readRequest be reader.[[readRequests]][0]. + // Step 3.3. Remove readRequest from reader.[[readRequests]]. + RefPtr<ReadRequest> readRequest = reader->ReadRequests().popFirst(); + + // Step 3.4. Perform ! + // ReadableByteStreamControllerFillReadRequestFromQueue(controller, + // readRequest). + ReadableByteStreamControllerFillReadRequestFromQueue(aCx, aController, + readRequest, aRv); + if (aRv.Failed()) { + return; + } + } +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-enqueue +void ReadableByteStreamControllerEnqueue( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aChunk, ErrorResult& aRv) { + aRv.MightThrowJSException(); + + // Step 1. + RefPtr<ReadableStream> stream = aController->Stream(); + + // Step 2. + if (aController->CloseRequested() || + stream->State() != ReadableStream::ReaderState::Readable) { + return; + } + + // Step 3. + bool isShared; + JS::Rooted<JSObject*> buffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, aChunk, &isShared)); + if (!buffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // Step 4. + size_t byteOffset = JS_GetArrayBufferViewByteOffset(aChunk); + + // Step 5. + size_t byteLength = JS_GetArrayBufferViewByteLength(aChunk); + + // Step 6. + if (JS::IsDetachedArrayBufferObject(buffer)) { + aRv.ThrowTypeError("Detached Array Buffer"); + return; + } + + // Step 7. + JS::Rooted<JSObject*> transferredBuffer(aCx, + TransferArrayBuffer(aCx, buffer)); + if (!transferredBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // Step 8. + if (!aController->PendingPullIntos().isEmpty()) { + // Step 8.1 + RefPtr<PullIntoDescriptor> firstPendingPullInto = + aController->PendingPullIntos().getFirst(); + + // Step 8.2 + JS::Rooted<JSObject*> pendingBuffer(aCx, firstPendingPullInto->Buffer()); + if (JS::IsDetachedArrayBufferObject(pendingBuffer)) { + aRv.ThrowTypeError("Pending PullInto has detached buffer"); + return; + } + + // Step 8.3. Perform ! + // ReadableByteStreamControllerInvalidateBYOBRequest(controller). + ReadableByteStreamControllerInvalidateBYOBRequest(aController); + + // Step 8.4. Set firstPendingPullInto’s buffer to ! + // TransferArrayBuffer(firstPendingPullInto’s buffer). + pendingBuffer = TransferArrayBuffer(aCx, pendingBuffer); + if (!pendingBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + firstPendingPullInto->SetBuffer(pendingBuffer); + + // Step 8.5. If firstPendingPullInto’s reader type is "none", perform ? + // ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, + // firstPendingPullInto). + if (firstPendingPullInto->GetReaderType() == ReaderType::None) { + ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue( + aCx, aController, firstPendingPullInto, aRv); + if (aRv.Failed()) { + return; + } + } + } + + // Step 9. If ! ReadableStreamHasDefaultReader(stream) is true, + if (ReadableStreamHasDefaultReader(stream)) { + // Step 9.1. Perform ! + // ReadableByteStreamControllerProcessReadRequestsUsingQueue(controller). + ReadableByteStreamControllerProcessReadRequestsUsingQueue(aCx, aController, + aRv); + if (aRv.Failed()) { + return; + } + + // Step 9.2. If ! ReadableStreamGetNumReadRequests(stream) is 0, + if (ReadableStreamGetNumReadRequests(stream) == 0) { + // Step 9.2.1 Assert: controller.[[pendingPullIntos]] is empty. + MOZ_ASSERT(aController->PendingPullIntos().isEmpty()); + + // Step 9.2.2. Perform ! + // ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, byteOffset, byteLength). + ReadableByteStreamControllerEnqueueChunkToQueue( + aController, transferredBuffer, byteOffset, byteLength); + + // Step 9.3. Otherwise, + } else { + // Step 9.3.1 Assert: controller.[[queue]] is empty. + MOZ_ASSERT(aController->Queue().isEmpty()); + + // Step 9.3.2. If controller.[[pendingPullIntos]] is not empty, + if (!aController->PendingPullIntos().isEmpty()) { + // Step 9.3.2.1. Assert: controller.[[pendingPullIntos]][0]'s reader + // type is "default". + MOZ_ASSERT( + aController->PendingPullIntos().getFirst()->GetReaderType() == + ReaderType::Default); + + // Step 9.3.2.2. Perform ! + // ReadableByteStreamControllerShiftPendingPullInto(controller). + RefPtr<PullIntoDescriptor> pullIntoDescriptor = + ReadableByteStreamControllerShiftPendingPullInto(aController); + (void)pullIntoDescriptor; + } + + // Step 9.3.3. Let transferredView be ! Construct(%Uint8Array%, « + // transferredBuffer, byteOffset, byteLength »). + JS::Rooted<JSObject*> transferredView( + aCx, JS_NewUint8ArrayWithBuffer(aCx, transferredBuffer, byteOffset, + int64_t(byteLength))); + if (!transferredView) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // Step 9.3.4. Perform ! ReadableStreamFulfillReadRequest(stream, + // transferredView, false). + JS::Rooted<JS::Value> transferredViewValue( + aCx, JS::ObjectValue(*transferredView)); + ReadableStreamFulfillReadRequest(aCx, stream, transferredViewValue, false, + aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 10. Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true, + } else if (ReadableStreamHasBYOBReader(stream)) { + // Step 10.1. Perform ! + // ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, byteOffset, byteLength). + ReadableByteStreamControllerEnqueueChunkToQueue( + aController, transferredBuffer, byteOffset, byteLength); + + // Step 10.2 Perform ! + // ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( + aCx, aController, aRv); + if (aRv.Failed()) { + return; + } + + // Step 11. Otherwise, + } else { + // Step 11.1. Assert: ! IsReadableStreamLocked(stream) is false. + MOZ_ASSERT(!IsReadableStreamLocked(stream)); + + // Step 11.2. Perform ! + // ReadableByteStreamControllerEnqueueChunkToQueue(controller, + // transferredBuffer, byteOffset, byteLength). + ReadableByteStreamControllerEnqueueChunkToQueue( + aController, transferredBuffer, byteOffset, byteLength); + } + + // Step 12. Perform ! + // ReadableByteStreamControllerCallPullIfNeeded(controller). + ReadableByteStreamControllerCallPullIfNeeded(aCx, aController, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rbs-controller-enqueue +void ReadableByteStreamController::Enqueue(JSContext* aCx, + const ArrayBufferView& aChunk, + ErrorResult& aRv) { + // Step 1. + JS::Rooted<JSObject*> chunk(aCx, aChunk.Obj()); + if (JS_GetArrayBufferViewByteLength(chunk) == 0) { + aRv.ThrowTypeError("Zero Length View"); + return; + } + + // Step 2. + bool isShared; + JS::Rooted<JSObject*> viewedArrayBuffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, chunk, &isShared)); + if (!viewedArrayBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + if (JS::GetArrayBufferByteLength(viewedArrayBuffer) == 0) { + aRv.ThrowTypeError("Zero Length Buffer"); + return; + } + + // Step 3. + if (CloseRequested()) { + aRv.ThrowTypeError("close requested"); + return; + } + + // Step 4. + if (Stream()->State() != ReadableStream::ReaderState::Readable) { + aRv.ThrowTypeError("Not Readable"); + return; + } + + // Step 5. + ReadableByteStreamControllerEnqueue(aCx, this, chunk, aRv); +} + +// https://streams.spec.whatwg.org/#rbs-controller-error +void ReadableByteStreamController::Error(JSContext* aCx, + JS::Handle<JS::Value> aErrorValue, + ErrorResult& aRv) { + // Step 1. + ReadableByteStreamControllerError(this, aErrorValue, aRv); +} + +// https://streams.spec.whatwg.org/#rbs-controller-private-cancel +already_AddRefed<Promise> ReadableByteStreamController::CancelSteps( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1. + ReadableByteStreamControllerClearPendingPullIntos(this); + + // Step 2. + ResetQueue(this); + + // Step 3. + Optional<JS::Handle<JS::Value>> reason(aCx, aReason); + RefPtr<UnderlyingSourceAlgorithmsBase> algorithms = mAlgorithms; + RefPtr<Promise> result = algorithms->CancelCallback(aCx, reason, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + // Step 4. + ReadableByteStreamControllerClearAlgorithms(this); + + // Step 5. + return result.forget(); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-handle-queue-drain +void ReadableByteStreamControllerHandleQueueDrain( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv) { + // Step 1. + MOZ_ASSERT(aController->Stream()->State() == + ReadableStream::ReaderState::Readable); + + // Step 2. + if (aController->QueueTotalSize() == 0 && aController->CloseRequested()) { + // Step 2.1 + ReadableByteStreamControllerClearAlgorithms(aController); + + // Step 2.2 + RefPtr<ReadableStream> stream = aController->Stream(); + ReadableStreamClose(aCx, stream, aRv); + return; + } + + // Step 3.1 + ReadableByteStreamControllerCallPullIfNeeded(aCx, aController, aRv); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rbs-controller-private-pull +void ReadableByteStreamController::PullSteps(JSContext* aCx, + ReadRequest* aReadRequest, + ErrorResult& aRv) { + // Step 1. + ReadableStream* stream = Stream(); + + // Step 2. + MOZ_ASSERT(ReadableStreamHasDefaultReader(stream)); + + // Step 3. + if (QueueTotalSize() > 0) { + // Step 3.1. Assert: ! ReadableStreamGetNumReadRequests ( stream ) is 0. + MOZ_ASSERT(ReadableStreamGetNumReadRequests(stream) == 0); + + // Step 3.2. Perform ! + // ReadableByteStreamControllerFillReadRequestFromQueue(this, readRequest). + ReadableByteStreamControllerFillReadRequestFromQueue(aCx, this, + aReadRequest, aRv); + + // Step 3.3. Return. + return; + } + + // Step 4. + Maybe<uint64_t> autoAllocateChunkSize = AutoAllocateChunkSize(); + + // Step 5. + if (autoAllocateChunkSize) { + // Step 5.1 + aRv.MightThrowJSException(); + JS::Rooted<JSObject*> buffer( + aCx, JS::NewArrayBuffer(aCx, *autoAllocateChunkSize)); + // Step 5.2 + if (!buffer) { + // Step 5.2.1 + JS::Rooted<JS::Value> bufferError(aCx); + if (!JS_GetPendingException(aCx, &bufferError)) { + // Uncatchable exception; we should mark aRv and return. + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // It's not expliclitly stated, but I assume the intention here is that + // we perform a normal completion here. + JS_ClearPendingException(aCx); + + aReadRequest->ErrorSteps(aCx, bufferError, aRv); + + // Step 5.2.2. + return; + } + + // Step 5.3 + RefPtr<PullIntoDescriptor> pullIntoDescriptor = new PullIntoDescriptor( + buffer, *autoAllocateChunkSize, 0, *autoAllocateChunkSize, 0, 1, + PullIntoDescriptor::Constructor::Uint8, ReaderType::Default); + + // Step 5.4 + PendingPullIntos().insertBack(pullIntoDescriptor); + } + + // Step 6. + ReadableStreamAddReadRequest(stream, aReadRequest); + + // Step 7. + ReadableByteStreamControllerCallPullIfNeeded(aCx, this, aRv); +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamcontroller-releasesteps +void ReadableByteStreamController::ReleaseSteps() { + // Step 1. If this.[[pendingPullIntos]] is not empty, + if (!PendingPullIntos().isEmpty()) { + // Step 1.1. Let firstPendingPullInto be this.[[pendingPullIntos]][0]. + RefPtr<PullIntoDescriptor> firstPendingPullInto = + PendingPullIntos().popFirst(); + + // Step 1.2. Set firstPendingPullInto’s reader type to "none". + firstPendingPullInto->SetReaderType(ReaderType::None); + + // Step 1.3. Set this.[[pendingPullIntos]] to the list « + // firstPendingPullInto ». + PendingPullIntos().clear(); + PendingPullIntos().insertBack(firstPendingPullInto); + } +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-shift-pending-pull-into +already_AddRefed<PullIntoDescriptor> +ReadableByteStreamControllerShiftPendingPullInto( + ReadableByteStreamController* aController) { + // Step 1. + MOZ_ASSERT(!aController->GetByobRequest()); + + // Step 2 + 3 + RefPtr<PullIntoDescriptor> descriptor = + aController->PendingPullIntos().popFirst(); + + // Step 4. + return descriptor.forget(); +} + +JSObject* ConstructFromPullIntoConstructor( + JSContext* aCx, PullIntoDescriptor::Constructor constructor, + JS::Handle<JSObject*> buffer, size_t byteOffset, size_t length) { + switch (constructor) { + case PullIntoDescriptor::Constructor::DataView: + return JS_NewDataView(aCx, buffer, byteOffset, length); + break; + +#define CONSTRUCT_TYPED_ARRAY_TYPE(ExternalT, NativeT, Name) \ + case PullIntoDescriptor::Constructor::Name: \ + return JS_New##Name##ArrayWithBuffer(aCx, buffer, byteOffset, \ + int64_t(length)); \ + break; + + JS_FOR_EACH_TYPED_ARRAY(CONSTRUCT_TYPED_ARRAY_TYPE) + +#undef CONSTRUCT_TYPED_ARRAY_TYPE + + default: + MOZ_ASSERT_UNREACHABLE("Unknown PullIntoDescriptor::Constructor"); + return nullptr; + } +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-convert-pull-into-descriptor +JSObject* ReadableByteStreamControllerConvertPullIntoDescriptor( + JSContext* aCx, PullIntoDescriptor* pullIntoDescriptor, ErrorResult& aRv) { + // Step 1. Let bytesFilled be pullIntoDescriptor’s bytes filled. + uint64_t bytesFilled = pullIntoDescriptor->BytesFilled(); + + // Step 2. Let elementSize be pullIntoDescriptor’s element size. + uint64_t elementSize = pullIntoDescriptor->ElementSize(); + + // Step 3. Assert: bytesFilled ≤ pullIntoDescriptor’s byte length. + MOZ_ASSERT(bytesFilled <= pullIntoDescriptor->ByteLength()); + + // Step 4. Assert: bytesFilled mod elementSize is 0. + MOZ_ASSERT(bytesFilled % elementSize == 0); + + // Step 5. Let buffer be ! TransferArrayBuffer(pullIntoDescriptor’s buffer). + aRv.MightThrowJSException(); + JS::Rooted<JSObject*> srcBuffer(aCx, pullIntoDescriptor->Buffer()); + JS::Rooted<JSObject*> buffer(aCx, TransferArrayBuffer(aCx, srcBuffer)); + if (!buffer) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 6. Return ! Construct(pullIntoDescriptor’s view constructor, + // « buffer, pullIntoDescriptor’s byte offset, bytesFilled ÷ elementSize »). + JS::Rooted<JSObject*> res( + aCx, ConstructFromPullIntoConstructor( + aCx, pullIntoDescriptor->ViewConstructor(), buffer, + pullIntoDescriptor->ByteOffset(), bytesFilled / elementSize)); + if (!res) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + return res; +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-respond-in-closed-state +MOZ_CAN_RUN_SCRIPT +static void ReadableByteStreamControllerRespondInClosedState( + JSContext* aCx, ReadableByteStreamController* aController, + RefPtr<PullIntoDescriptor>& aFirstDescriptor, ErrorResult& aRv) { + // Step 1. Assert: firstDescriptor ’s bytes filled is 0. + MOZ_ASSERT(aFirstDescriptor->BytesFilled() == 0); + + // Step 2. If firstDescriptor’s reader type is "none", + // perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + if (aFirstDescriptor->GetReaderType() == ReaderType::None) { + RefPtr<PullIntoDescriptor> discarded = + ReadableByteStreamControllerShiftPendingPullInto(aController); + (void)discarded; + } + + // Step 3. Let stream be controller.[[stream]]. + RefPtr<ReadableStream> stream = aController->Stream(); + + // Step 4. If ! ReadableStreamHasBYOBReader(stream) is true, + if (ReadableStreamHasBYOBReader(stream)) { + // Step 4.1. While ! ReadableStreamGetNumReadIntoRequests(stream) > 0, + while (ReadableStreamGetNumReadIntoRequests(stream) > 0) { + // Step 4.1.1. Let pullIntoDescriptor be ! + // ReadableByteStreamControllerShiftPendingPullInto(controller). + RefPtr<PullIntoDescriptor> pullIntoDescriptor = + ReadableByteStreamControllerShiftPendingPullInto(aController); + + // Step 4.1.2. Perform ! + // ReadableByteStreamControllerCommitPullIntoDescriptor(stream, + // pullIntoDescriptor). + ReadableByteStreamControllerCommitPullIntoDescriptor( + aCx, stream, pullIntoDescriptor, aRv); + } + } +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-fill-head-pull-into-descriptor +void ReadableByteStreamControllerFillHeadPullIntoDescriptor( + ReadableByteStreamController* aController, size_t aSize, + PullIntoDescriptor* aPullIntoDescriptor) { + // Step 1. Assert: either controller.[[pendingPullIntos]] is empty, or + // controller.[[pendingPullIntos]][0] is pullIntoDescriptor. + MOZ_ASSERT(aController->PendingPullIntos().isEmpty() || + aController->PendingPullIntos().getFirst() == aPullIntoDescriptor); + + // Step 2. Assert: controller.[[byobRequest]] is null. + MOZ_ASSERT(!aController->GetByobRequest()); + + // Step 3. Set pullIntoDescriptor’s bytes filled to bytes filled + size. + aPullIntoDescriptor->SetBytesFilled(aPullIntoDescriptor->BytesFilled() + + aSize); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-respond-in-readable-state +MOZ_CAN_RUN_SCRIPT +static void ReadableByteStreamControllerRespondInReadableState( + JSContext* aCx, ReadableByteStreamController* aController, + uint64_t aBytesWritten, PullIntoDescriptor* aPullIntoDescriptor, + ErrorResult& aRv) { + // Step 1. Assert: pullIntoDescriptor’s bytes filled + bytesWritten ≤ + // pullIntoDescriptor’s byte length. + MOZ_ASSERT(aPullIntoDescriptor->BytesFilled() + aBytesWritten <= + aPullIntoDescriptor->ByteLength()); + + // Step 2. Perform + // !ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, + // bytesWritten, pullIntoDescriptor). + ReadableByteStreamControllerFillHeadPullIntoDescriptor( + aController, aBytesWritten, aPullIntoDescriptor); + + // Step 3. If pullIntoDescriptor’s reader type is "none", + if (aPullIntoDescriptor->GetReaderType() == ReaderType::None) { + // Step 3.1. Perform ? + // ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, + // pullIntoDescriptor). + ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue( + aCx, aController, aPullIntoDescriptor, aRv); + if (aRv.Failed()) { + return; + } + + // Step 3.2. Perform ! + // ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( + aCx, aController, aRv); + + // Step 3.3. Return. + return; + } + + // Step 4. If pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s element + // size, return. + if (aPullIntoDescriptor->BytesFilled() < aPullIntoDescriptor->ElementSize()) { + return; + } + + // Step 5. Perform + // !ReadableByteStreamControllerShiftPendingPullInto(controller). + RefPtr<PullIntoDescriptor> pullIntoDescriptor = + ReadableByteStreamControllerShiftPendingPullInto(aController); + (void)pullIntoDescriptor; + + // Step 6. Let remainderSize be pullIntoDescriptor’s bytes filled mod + // pullIntoDescriptor’s element size. + size_t remainderSize = + aPullIntoDescriptor->BytesFilled() % aPullIntoDescriptor->ElementSize(); + + // Step 7. If remainderSize > 0, + if (remainderSize > 0) { + // Step 7.1. Let end be pullIntoDescriptor’s byte offset + + // pullIntoDescriptor’s bytes filled. + size_t end = + aPullIntoDescriptor->ByteOffset() + aPullIntoDescriptor->BytesFilled(); + + // Step 7.2. Perform ? + // ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, + // pullIntoDescriptor’s buffer, end − remainderSize, remainderSize). + JS::Rooted<JSObject*> pullIntoBuffer(aCx, aPullIntoDescriptor->Buffer()); + ReadableByteStreamControllerEnqueueClonedChunkToQueue( + aCx, aController, pullIntoBuffer, end - remainderSize, remainderSize, + aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 8. Set pullIntoDescriptor’s bytes filled to pullIntoDescriptor’s bytes + // filled − remainderSize. + aPullIntoDescriptor->SetBytesFilled(aPullIntoDescriptor->BytesFilled() - + remainderSize); + + // Step 9. Perform + // !ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[stream]], + // pullIntoDescriptor). + RefPtr<ReadableStream> stream(aController->Stream()); + ReadableByteStreamControllerCommitPullIntoDescriptor( + aCx, stream, aPullIntoDescriptor, aRv); + if (aRv.Failed()) { + return; + } + + // Step 10. Perform + // !ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue( + aCx, aController, aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-respond-internal +void ReadableByteStreamControllerRespondInternal( + JSContext* aCx, ReadableByteStreamController* aController, + uint64_t aBytesWritten, ErrorResult& aRv) { + // Step 1. + RefPtr<PullIntoDescriptor> firstDescriptor = + aController->PendingPullIntos().getFirst(); + + // Step 2. + JS::Rooted<JSObject*> buffer(aCx, firstDescriptor->Buffer()); +#ifdef DEBUG + bool canTransferBuffer = CanTransferArrayBuffer(aCx, buffer, aRv); + MOZ_ASSERT(!aRv.Failed()); + MOZ_ASSERT(canTransferBuffer); +#endif + + // Step 3. + ReadableByteStreamControllerInvalidateBYOBRequest(aController); + + // Step 4. + auto state = aController->Stream()->State(); + + // Step 5. + if (state == ReadableStream::ReaderState::Closed) { + // Step 5.1 + MOZ_ASSERT(aBytesWritten == 0); + + // Step 5.2 + ReadableByteStreamControllerRespondInClosedState(aCx, aController, + firstDescriptor, aRv); + if (aRv.Failed()) { + return; + } + } else { + // Step 6.1 + MOZ_ASSERT(state == ReadableStream::ReaderState::Readable); + + // Step 6.2. + MOZ_ASSERT(aBytesWritten > 0); + + // Step 6.3 + ReadableByteStreamControllerRespondInReadableState( + aCx, aController, aBytesWritten, firstDescriptor, aRv); + if (aRv.Failed()) { + return; + } + } + // Step 7. + ReadableByteStreamControllerCallPullIfNeeded(aCx, aController, aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-respond +void ReadableByteStreamControllerRespond( + JSContext* aCx, ReadableByteStreamController* aController, + uint64_t aBytesWritten, ErrorResult& aRv) { + // Step 1. + MOZ_ASSERT(!aController->PendingPullIntos().isEmpty()); + + // Step 2. + PullIntoDescriptor* firstDescriptor = + aController->PendingPullIntos().getFirst(); + + // Step 3. + auto state = aController->Stream()->State(); + + // Step 4. + if (state == ReadableStream::ReaderState::Closed) { + // Step 4.1 + if (aBytesWritten != 0) { + aRv.ThrowTypeError("bytesWritten not zero on closed stream"); + return; + } + } else { + // Step 5.1 + MOZ_ASSERT(state == ReadableStream::ReaderState::Readable); + + // Step 5.2 + if (aBytesWritten == 0) { + aRv.ThrowTypeError("bytesWritten 0"); + return; + } + + // Step 5.3 + if (firstDescriptor->BytesFilled() + aBytesWritten > + firstDescriptor->ByteLength()) { + aRv.ThrowRangeError("bytesFilled + bytesWritten > byteLength"); + return; + } + } + + // Step 6. + aRv.MightThrowJSException(); + JS::Rooted<JSObject*> buffer(aCx, firstDescriptor->Buffer()); + JS::Rooted<JSObject*> transferredBuffer(aCx, + TransferArrayBuffer(aCx, buffer)); + if (!transferredBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + firstDescriptor->SetBuffer(transferredBuffer); + + // Step 7. + ReadableByteStreamControllerRespondInternal(aCx, aController, aBytesWritten, + aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-respond-with-new-view +void ReadableByteStreamControllerRespondWithNewView( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aView, ErrorResult& aRv) { + aRv.MightThrowJSException(); + + // Step 1. + MOZ_ASSERT(!aController->PendingPullIntos().isEmpty()); + + // Step 2. + bool isSharedMemory; + JS::Rooted<JSObject*> viewedArrayBuffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, aView, &isSharedMemory)); + if (!viewedArrayBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + MOZ_ASSERT(!JS::IsDetachedArrayBufferObject(viewedArrayBuffer)); + + // Step 3. + RefPtr<PullIntoDescriptor> firstDescriptor = + aController->PendingPullIntos().getFirst(); + + // Step 4. + ReadableStream::ReaderState state = aController->Stream()->State(); + + // Step 5. + if (state == ReadableStream::ReaderState::Closed) { + // Step 5.1 + if (JS_GetArrayBufferViewByteLength(aView) != 0) { + aRv.ThrowTypeError("View has non-zero length in closed stream"); + return; + } + } else { + // Step 6.1 + MOZ_ASSERT(state == ReadableStream::ReaderState::Readable); + + // Step 6.2 + if (JS_GetArrayBufferViewByteLength(aView) == 0) { + aRv.ThrowTypeError("View has zero length in readable stream"); + return; + } + } + + // Step 7. + if (firstDescriptor->ByteOffset() + firstDescriptor->BytesFilled() != + JS_GetArrayBufferViewByteOffset(aView)) { + aRv.ThrowRangeError("Invalid Offset"); + return; + } + + // Step 8. + if (firstDescriptor->BufferByteLength() != + JS::GetArrayBufferByteLength(viewedArrayBuffer)) { + aRv.ThrowRangeError("Mismatched buffer byte lengths"); + return; + } + + // Step 9. + if (firstDescriptor->BytesFilled() + JS_GetArrayBufferViewByteLength(aView) > + firstDescriptor->ByteLength()) { + aRv.ThrowRangeError("Too many bytes"); + return; + } + + // Step 10. Let viewByteLength be view.[[ByteLength]]. + size_t viewByteLength = JS_GetArrayBufferViewByteLength(aView); + + // Step 11. Set firstDescriptor’s buffer to ? + // TransferArrayBuffer(view.[[ViewedArrayBuffer]]). + JS::Rooted<JSObject*> transferedBuffer( + aCx, TransferArrayBuffer(aCx, viewedArrayBuffer)); + if (!transferedBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + firstDescriptor->SetBuffer(transferedBuffer); + + // Step 12. Perform ? ReadableByteStreamControllerRespondInternal(controller, + // viewByteLength). + ReadableByteStreamControllerRespondInternal(aCx, aController, viewByteLength, + aRv); +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-fill-pull-into-descriptor-from-queue +bool ReadableByteStreamControllerFillPullIntoDescriptorFromQueue( + JSContext* aCx, ReadableByteStreamController* aController, + PullIntoDescriptor* aPullIntoDescriptor, ErrorResult& aRv) { + // Step 1. Let elementSize be pullIntoDescriptor.[[elementSize]]. + size_t elementSize = aPullIntoDescriptor->ElementSize(); + + // Step 2. Let currentAlignedBytes be pullIntoDescriptor’s bytes filled − + // (pullIntoDescriptor’s bytes filled mod elementSize). + size_t currentAlignedBytes = + aPullIntoDescriptor->BytesFilled() - + (aPullIntoDescriptor->BytesFilled() % elementSize); + + // Step 3. Let maxBytesToCopy be min(controller.[[queueTotalSize]], + // pullIntoDescriptor’s byte length − pullIntoDescriptor’s bytes filled). + size_t maxBytesToCopy = + std::min(static_cast<size_t>(aController->QueueTotalSize()), + static_cast<size_t>((aPullIntoDescriptor->ByteLength() - + aPullIntoDescriptor->BytesFilled()))); + + // Step 4. Let maxBytesFilled be pullIntoDescriptor’s bytes filled + + // maxBytesToCopy. + size_t maxBytesFilled = aPullIntoDescriptor->BytesFilled() + maxBytesToCopy; + + // Step 5. Let maxAlignedBytes be maxBytesFilled − (maxBytesFilled mod + // elementSize). + size_t maxAlignedBytes = maxBytesFilled - (maxBytesFilled % elementSize); + + // Step 6. Let totalBytesToCopyRemaining be maxBytesToCopy. + size_t totalBytesToCopyRemaining = maxBytesToCopy; + + // Step 7. Let ready be false. + bool ready = false; + + // Step 8. If maxAlignedBytes > currentAlignedBytes, + if (maxAlignedBytes > currentAlignedBytes) { + // Step 8.1. Set totalBytesToCopyRemaining to maxAlignedBytes − + // pullIntoDescriptor’s bytes filled. + totalBytesToCopyRemaining = + maxAlignedBytes - aPullIntoDescriptor->BytesFilled(); + // Step 8.2. Set ready to true. + ready = true; + } + + // Step 9. Let queue be controller.[[queue]]. + LinkedList<RefPtr<ReadableByteStreamQueueEntry>>& queue = + aController->Queue(); + + // Step 10. While totalBytesToCopyRemaining > 0, + while (totalBytesToCopyRemaining > 0) { + // Step 10.1 Let headOfQueue be queue[0]. + ReadableByteStreamQueueEntry* headOfQueue = queue.getFirst(); + + // Step 10.2. Let bytesToCopy be min(totalBytesToCopyRemaining, + // headOfQueue’s byte length). + size_t bytesToCopy = + std::min(totalBytesToCopyRemaining, headOfQueue->ByteLength()); + + // Step 10.3. Let destStart be pullIntoDescriptor’s byte offset + + // pullIntoDescriptor’s bytes filled. + size_t destStart = + aPullIntoDescriptor->ByteOffset() + aPullIntoDescriptor->BytesFilled(); + + // Step 10.4. Perform !CopyDataBlockBytes(pullIntoDescriptor’s + // buffer.[[ArrayBufferData]], destStart, headOfQueue’s + // buffer.[[ArrayBufferData]], headOfQueue’s byte offset, + // bytesToCopy). + JS::Rooted<JSObject*> descriptorBuffer(aCx, aPullIntoDescriptor->Buffer()); + JS::Rooted<JSObject*> queueBuffer(aCx, headOfQueue->Buffer()); + if (!JS::ArrayBufferCopyData(aCx, descriptorBuffer, destStart, queueBuffer, + headOfQueue->ByteOffset(), bytesToCopy)) { + aRv.StealExceptionFromJSContext(aCx); + return false; + } + + // Step 10.5. If headOfQueue’s byte length is bytesToCopy, + if (headOfQueue->ByteLength() == bytesToCopy) { + // Step 10.5.1. Remove queue[0]. + queue.popFirst(); + } else { + // Step 10.6. Otherwise, + + // Step 10.6.1 Set headOfQueue’s byte offset to + // headOfQueue’s byte offset + bytesToCopy. + headOfQueue->SetByteOffset(headOfQueue->ByteOffset() + bytesToCopy); + // Step 10.6.2 Set headOfQueue’s byte length to + // headOfQueue’s byte length − bytesToCopy. + headOfQueue->SetByteLength(headOfQueue->ByteLength() - bytesToCopy); + } + + // Step 10.7. Set controller.[[queueTotalSize]] to + // controller.[[queueTotalSize]] − bytesToCopy. + aController->SetQueueTotalSize(aController->QueueTotalSize() - + (double)bytesToCopy); + + // Step 10.8, Perform + // !ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, + // bytesToCopy, pullIntoDescriptor). + ReadableByteStreamControllerFillHeadPullIntoDescriptor( + aController, bytesToCopy, aPullIntoDescriptor); + + // Step 10.9. Set totalBytesToCopyRemaining to totalBytesToCopyRemaining − + // bytesToCopy. + totalBytesToCopyRemaining = totalBytesToCopyRemaining - bytesToCopy; + } + + // Step 11. If ready is false, + if (!ready) { + // Step 11.1. Assert: controller.[[queueTotalSize]] is 0. + MOZ_ASSERT(aController->QueueTotalSize() == 0); + + // Step 11.2. Assert: pullIntoDescriptor’s bytes filled > 0. + MOZ_ASSERT(aPullIntoDescriptor->BytesFilled() > 0); + + // Step 11.3. Assert: pullIntoDescriptor’s bytes filled < + // pullIntoDescriptor’s + // element size. + MOZ_ASSERT(aPullIntoDescriptor->BytesFilled() < + aPullIntoDescriptor->ElementSize()); + } + + // Step 12. Return ready. + return ready; +} + +// https://streams.spec.whatwg.org/#readable-byte-stream-controller-pull-into +void ReadableByteStreamControllerPullInto( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aView, ReadIntoRequest* aReadIntoRequest, + ErrorResult& aRv) { + aRv.MightThrowJSException(); + + // Step 1. Let stream be controller.[[stream]]. + ReadableStream* stream = aController->Stream(); + + // Step 2. Let elementSize be 1. + size_t elementSize = 1; + + // Step 3. Let ctor be %DataView%. + PullIntoDescriptor::Constructor ctor = + PullIntoDescriptor::Constructor::DataView; + + // Step 4. If view has a [[TypedArrayName]] internal slot (i.e., it is not a + // DataView), + if (JS_IsTypedArrayObject(aView)) { + // Step 4.1. Set elementSize to the element size specified in the typed + // array constructors table for view.[[TypedArrayName]]. + JS::Scalar::Type type = JS_GetArrayBufferViewType(aView); + elementSize = JS::Scalar::byteSize(type); + + // Step 4.2 Set ctor to the constructor specified in the typed array + // constructors table for view.[[TypedArrayName]]. + ctor = PullIntoDescriptor::constructorFromScalar(type); + } + + // Step 5. Let byteOffset be view.[[ByteOffset]]. + size_t byteOffset = JS_GetArrayBufferViewByteOffset(aView); + + // Step 6. Let byteLength be view.[[ByteLength]]. + size_t byteLength = JS_GetArrayBufferViewByteLength(aView); + + // Step 7. Let bufferResult be + // TransferArrayBuffer(view.[[ViewedArrayBuffer]]). + bool isShared; + JS::Rooted<JSObject*> viewedArrayBuffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, aView, &isShared)); + if (!viewedArrayBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + JS::Rooted<JSObject*> bufferResult( + aCx, TransferArrayBuffer(aCx, viewedArrayBuffer)); + + // Step 8. If bufferResult is an abrupt completion, + if (!bufferResult) { + JS::Rooted<JS::Value> pendingException(aCx); + if (!JS_GetPendingException(aCx, &pendingException)) { + // This means an un-catchable exception. Use StealExceptionFromJSContext + // to setup aRv properly. + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // It's not expliclitly stated, but I assume the intention here is that + // we perform a normal completion here; we also need to clear the + // exception state anyhow to succesfully run ErrorSteps. + JS_ClearPendingException(aCx); + + // Step 8.1. Perform readIntoRequest’s error steps, given + // bufferResult.[[Value]]. + aReadIntoRequest->ErrorSteps(aCx, pendingException, aRv); + + // Step 8.2. Return. + return; + } + + // Step 9. Let buffer be bufferResult.[[Value]]. + JS::Rooted<JSObject*> buffer(aCx, bufferResult); + + // Step 10. Let pullIntoDescriptor be a new pull-into descriptor with + // buffer: buffer, + // buffer byte length: buffer.[[ArrayBufferByteLength]], + // byte offset: byteOffset, + // byte length: byteLength, + // bytes filled: 0, + // element size: elementSize, + // view constructor: ctor, + // and reader type: "byob". + RefPtr<PullIntoDescriptor> pullIntoDescriptor = new PullIntoDescriptor( + buffer, JS::GetArrayBufferByteLength(buffer), byteOffset, byteLength, 0, + elementSize, ctor, ReaderType::BYOB); + + // Step 11. If controller.[[pendingPullIntos]] is not empty, + if (!aController->PendingPullIntos().isEmpty()) { + // Step 11.1. Append pullIntoDescriptor to controller.[[pendingPullIntos]]. + aController->PendingPullIntos().insertBack(pullIntoDescriptor); + + // Step 11.2. Perform !ReadableStreamAddReadIntoRequest(stream, + // readIntoRequest). + ReadableStreamAddReadIntoRequest(stream, aReadIntoRequest); + + // Step 11.3. Return. + return; + } + + // Step 12. If stream.[[state]] is "closed", + if (stream->State() == ReadableStream::ReaderState::Closed) { + // Step 12.1. Let emptyView be !Construct(ctor, « pullIntoDescriptor’s + // buffer, pullIntoDescriptor’s byte offset, 0 »). + JS::Rooted<JSObject*> pullIntoBuffer(aCx, pullIntoDescriptor->Buffer()); + JS::Rooted<JSObject*> emptyView( + aCx, + ConstructFromPullIntoConstructor(aCx, ctor, pullIntoBuffer, + pullIntoDescriptor->ByteOffset(), 0)); + if (!emptyView) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + // Step 12.2. Perform readIntoRequest’s close steps, given emptyView. + JS::Rooted<JS::Value> emptyViewValue(aCx, JS::ObjectValue(*emptyView)); + aReadIntoRequest->CloseSteps(aCx, emptyViewValue, aRv); + + // Step 12.3. Return. + return; + } + + // Step 13,. If controller.[[queueTotalSize]] > 0, + if (aController->QueueTotalSize() > 0) { + // Step 13.1 If + // !ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, + // pullIntoDescriptor) is true, + bool ready = ReadableByteStreamControllerFillPullIntoDescriptorFromQueue( + aCx, aController, pullIntoDescriptor, aRv); + if (aRv.Failed()) { + return; + } + if (ready) { + // Step 13.1.1 Let filledView be + // !ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). + JS::Rooted<JSObject*> filledView( + aCx, ReadableByteStreamControllerConvertPullIntoDescriptor( + aCx, pullIntoDescriptor, aRv)); + if (aRv.Failed()) { + return; + } + // Step 13.1.2. Perform + // !ReadableByteStreamControllerHandleQueueDrain(controller). + ReadableByteStreamControllerHandleQueueDrain(aCx, aController, aRv); + if (aRv.Failed()) { + return; + } + // Step 13.1.3. Perform readIntoRequest’s chunk steps, given filledView. + JS::Rooted<JS::Value> filledViewValue(aCx, JS::ObjectValue(*filledView)); + aReadIntoRequest->ChunkSteps(aCx, filledViewValue, aRv); + // Step 13.1.4. Return. + return; + } + + // Step 13.2 If controller.[[closeRequested]] is true, + if (aController->CloseRequested()) { + // Step 13.2.1. Let e be a TypeError exception. + ErrorResult typeError; + typeError.ThrowTypeError("Close Requested True during Pull Into"); + + JS::Rooted<JS::Value> e(aCx); + MOZ_RELEASE_ASSERT(ToJSValue(aCx, std::move(typeError), &e)); + + // Step 13.2.2. Perform !ReadableByteStreamControllerError(controller, e). + ReadableByteStreamControllerError(aController, e, aRv); + if (aRv.Failed()) { + return; + } + + // Step 13.2.3. Perform readIntoRequest’s error steps, given e. + aReadIntoRequest->ErrorSteps(aCx, e, aRv); + + // Step 13.2.4. Return. + return; + } + } + + // Step 14. Append pullIntoDescriptor to controller.[[pendingPullIntos]]. + aController->PendingPullIntos().insertBack(pullIntoDescriptor); + + // Step 15. Perform !ReadableStreamAddReadIntoRequest(stream, + // readIntoRequest). + ReadableStreamAddReadIntoRequest(stream, aReadIntoRequest); + + // Step 16, Perform + // !ReadableByteStreamControllerCallPullIfNeeded(controller). + ReadableByteStreamControllerCallPullIfNeeded(aCx, aController, aRv); +} + +// https://streams.spec.whatwg.org/#set-up-readable-byte-stream-controller +void SetUpReadableByteStreamController( + JSContext* aCx, ReadableStream* aStream, + ReadableByteStreamController* aController, + UnderlyingSourceAlgorithmsBase* aAlgorithms, double aHighWaterMark, + Maybe<uint64_t> aAutoAllocateChunkSize, ErrorResult& aRv) { + // Step 1. Assert: stream.[[controller]] is undefined. + MOZ_ASSERT(!aStream->Controller()); + + // Step 2. If autoAllocateChunkSize is not undefined, + // Step 2.1. Assert: ! IsInteger(autoAllocateChunkSize) is true. Implicit + // Step 2.2. Assert: autoAllocateChunkSize is positive. (Implicit by + // type.) + + // Step 3. Set controller.[[stream]] to stream. + aController->SetStream(aStream); + + // Step 4. Set controller.[[pullAgain]] and controller.[[pulling]] to false. + aController->SetPullAgain(false); + aController->SetPulling(false); + + // Step 5. Set controller.[[byobRequest]] to null. + aController->SetByobRequest(nullptr); + + // Step 6. Perform !ResetQueue(controller). + ResetQueue(aController); + + // Step 7. Set controller.[[closeRequested]] and controller.[[started]] to + // false. + aController->SetCloseRequested(false); + aController->SetStarted(false); + + // Step 8. Set controller.[[strategyHWM]] to highWaterMark. + aController->SetStrategyHWM(aHighWaterMark); + + // Step 9. Set controller.[[pullAlgorithm]] to pullAlgorithm. + // Step 10. Set controller.[[cancelAlgorithm]] to cancelAlgorithm. + aController->SetAlgorithms(*aAlgorithms); + + // Step 11. Set controller.[[autoAllocateChunkSize]] to autoAllocateChunkSize. + aController->SetAutoAllocateChunkSize(aAutoAllocateChunkSize); + + // Step 12. Set controller.[[pendingPullIntos]] to a new empty list. + aController->PendingPullIntos().clear(); + + // Step 13. Set stream.[[controller]] to controller. + aStream->SetController(*aController); + + // Step 14. Let startResult be the result of performing startAlgorithm. + JS::Rooted<JS::Value> startResult(aCx, JS::UndefinedValue()); + RefPtr<ReadableStreamController> controller = aController; + aAlgorithms->StartCallback(aCx, *controller, &startResult, aRv); + if (aRv.Failed()) { + return; + } + + // Let startPromise be a promise resolved with startResult. + RefPtr<Promise> startPromise = + Promise::CreateInfallible(aStream->GetParentObject()); + startPromise->MaybeResolve(startResult); + + // Step 16+17 + startPromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableByteStreamController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(aController); + + // Step 16.1 + aController->SetStarted(true); + + // Step 16.2 + aController->SetPulling(false); + + // Step 16.3 + aController->SetPullAgain(false); + + // Step 16.4: + ReadableByteStreamControllerCallPullIfNeeded( + aCx, MOZ_KnownLive(aController), aRv); + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableByteStreamController* aController) { + // Step 17.1 + ReadableByteStreamControllerError(aController, aValue, aRv); + }, + RefPtr(aController)); +} + +// https://streams.spec.whatwg.org/#set-up-readable-byte-stream-controller-from-underlying-source +void SetUpReadableByteStreamControllerFromUnderlyingSource( + JSContext* aCx, ReadableStream* aStream, + JS::Handle<JSObject*> aUnderlyingSource, + UnderlyingSource& aUnderlyingSourceDict, double aHighWaterMark, + ErrorResult& aRv) { + // Step 1. Let controller be a new ReadableByteStreamController. + auto controller = + MakeRefPtr<ReadableByteStreamController>(aStream->GetParentObject()); + + // Step 2 - 7 + auto algorithms = MakeRefPtr<UnderlyingSourceAlgorithms>( + aStream->GetParentObject(), aUnderlyingSource, aUnderlyingSourceDict); + + // Step 8. Let autoAllocateChunkSize be + // underlyingSourceDict["autoAllocateChunkSize"], if it exists, or undefined + // otherwise. + Maybe<uint64_t> autoAllocateChunkSize = mozilla::Nothing(); + if (aUnderlyingSourceDict.mAutoAllocateChunkSize.WasPassed()) { + uint64_t value = aUnderlyingSourceDict.mAutoAllocateChunkSize.Value(); + // Step 9. If autoAllocateChunkSize is 0, then throw a TypeError + // exception. + if (value == 0) { + aRv.ThrowTypeError("autoAllocateChunkSize can not be zero."); + return; + } + autoAllocateChunkSize = mozilla::Some(value); + } + + // Step 10. Perform ? SetUpReadableByteStreamController(stream, controller, + // startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, + // autoAllocateChunkSize). + SetUpReadableByteStreamController(aCx, aStream, controller, algorithms, + aHighWaterMark, autoAllocateChunkSize, aRv); +} + +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableByteStreamController.h b/dom/streams/ReadableByteStreamController.h new file mode 100644 index 0000000000..45e03ba3b4 --- /dev/null +++ b/dom/streams/ReadableByteStreamController.h @@ -0,0 +1,230 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableByteStreamController_h +#define mozilla_dom_ReadableByteStreamController_h + +#include <cstddef> +#include "UnderlyingSourceCallbackHelpers.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/QueueWithSizes.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadRequest.h" +#include "mozilla/dom/ReadableStreamBYOBRequest.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/TypedArray.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/Nullable.h" +#include "nsTArray.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +// https://streams.spec.whatwg.org/#pull-into-descriptor-reader-type +// Indicates what type of readable stream reader initiated this request, +// or None if the initiating reader was released. +enum ReaderType { Default, BYOB, None }; + +struct PullIntoDescriptor; +struct ReadableByteStreamQueueEntry; +struct ReadIntoRequest; + +class ReadableByteStreamController final : public ReadableStreamController, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + ReadableByteStreamController, ReadableStreamController) + + public: + explicit ReadableByteStreamController(nsIGlobalObject* aGlobal); + + protected: + ~ReadableByteStreamController() override; + + public: + bool IsDefault() override { return false; } + bool IsByte() override { return true; } + ReadableStreamDefaultController* AsDefault() override { return nullptr; } + ReadableByteStreamController* AsByte() override { return this; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<ReadableStreamBYOBRequest> GetByobRequest(JSContext* aCx, + ErrorResult& aRv); + + Nullable<double> GetDesiredSize() const; + + MOZ_CAN_RUN_SCRIPT void Close(JSContext* aCx, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void Enqueue(JSContext* aCx, const ArrayBufferView& aChunk, + ErrorResult& aRv); + + void Error(JSContext* aCx, JS::Handle<JS::Value> aErrorValue, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelSteps( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) override; + MOZ_CAN_RUN_SCRIPT void PullSteps(JSContext* aCx, ReadRequest* aReadRequest, + ErrorResult& aRv) override; + void ReleaseSteps() override; + + // Internal Slot Accessors + Maybe<uint64_t> AutoAllocateChunkSize() { return mAutoAllocateChunkSize; } + void SetAutoAllocateChunkSize(Maybe<uint64_t>& aSize) { + mAutoAllocateChunkSize = aSize; + } + + ReadableStreamBYOBRequest* GetByobRequest() const { return mByobRequest; } + void SetByobRequest(ReadableStreamBYOBRequest* aByobRequest) { + mByobRequest = aByobRequest; + } + + LinkedList<RefPtr<PullIntoDescriptor>>& PendingPullIntos() { + return mPendingPullIntos; + } + void ClearPendingPullIntos(); + + double QueueTotalSize() const { return mQueueTotalSize; } + void SetQueueTotalSize(double aQueueTotalSize) { + mQueueTotalSize = aQueueTotalSize; + } + void AddToQueueTotalSize(double aLength) { mQueueTotalSize += aLength; } + + double StrategyHWM() const { return mStrategyHWM; } + void SetStrategyHWM(double aStrategyHWM) { mStrategyHWM = aStrategyHWM; } + + bool CloseRequested() const { return mCloseRequested; } + void SetCloseRequested(bool aCloseRequested) { + mCloseRequested = aCloseRequested; + } + + LinkedList<RefPtr<ReadableByteStreamQueueEntry>>& Queue() { return mQueue; } + void ClearQueue(); + + bool Started() const { return mStarted; } + void SetStarted(bool aStarted) { mStarted = aStarted; } + + bool Pulling() const { return mPulling; } + void SetPulling(bool aPulling) { mPulling = aPulling; } + + bool PullAgain() const { return mPullAgain; } + void SetPullAgain(bool aPullAgain) { mPullAgain = aPullAgain; } + + private: + // A boolean flag indicating whether the stream has been closed by its + // underlying byte source, but still has chunks in its internal queue that + // have not yet been read + bool mCloseRequested = false; + + // A boolean flag set to true if the stream’s mechanisms requested a call + // to the underlying byte source's pull algorithm to pull more data, but the + // pull could not yet be done since a previous call is still executing + bool mPullAgain = false; + + // A boolean flag indicating whether the underlying byte source has finished + // starting + bool mStarted = false; + + // A boolean flag set to true while the underlying byte source's pull + // algorithm is executing and the returned promise has not yet fulfilled, + // used to prevent reentrant calls + bool mPulling = false; + + // A positive integer, when the automatic buffer allocation feature is + // enabled. In that case, this value specifies the size of buffer to allocate. + // It is undefined otherwise. + Maybe<uint64_t> mAutoAllocateChunkSize; + + // A ReadableStreamBYOBRequest instance representing the current BYOB pull + // request, or null if there are no pending requests + RefPtr<ReadableStreamBYOBRequest> mByobRequest; + + // A list of pull-into descriptors + LinkedList<RefPtr<PullIntoDescriptor>> mPendingPullIntos; + + // A list of readable byte stream queue entries representing the stream’s + // internal queue of chunks + // + // This is theoretically supposed to be a QueueWithSizes, but it is + // mostly not actually manipulated or used like QueueWithSizes, so instead we + // use a LinkedList. + LinkedList<RefPtr<ReadableByteStreamQueueEntry>> mQueue; + + // The total size, in bytes, of all the chunks stored in [[queue]] (see § 8.1 + // Queue-with-sizes) + double mQueueTotalSize = 0.0; + + // A number supplied to the constructor as part of the stream’s queuing + // strategy, indicating the point at which the stream will apply backpressure + // to its underlying byte source + double mStrategyHWM = 0.0; +}; + +namespace streams_abstract { + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerRespond( + JSContext* aCx, ReadableByteStreamController* aController, + uint64_t aBytesWritten, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerRespondInternal( + JSContext* aCx, ReadableByteStreamController* aController, + uint64_t aBytesWritten, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerRespondWithNewView( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aView, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerPullInto( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aView, ReadIntoRequest* aReadIntoRequest, + ErrorResult& aRv); + +void ReadableByteStreamControllerError( + ReadableByteStreamController* aController, JS::Handle<JS::Value> aValue, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerEnqueue( + JSContext* aCx, ReadableByteStreamController* aController, + JS::Handle<JSObject*> aChunk, ErrorResult& aRv); + +already_AddRefed<ReadableStreamBYOBRequest> +ReadableByteStreamControllerGetBYOBRequest( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerClose( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void SetUpReadableByteStreamController( + JSContext* aCx, ReadableStream* aStream, + ReadableByteStreamController* aController, + UnderlyingSourceAlgorithmsBase* aAlgorithms, double aHighWaterMark, + Maybe<uint64_t> aAutoAllocateChunkSize, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamControllerCallPullIfNeeded( + JSContext* aCx, ReadableByteStreamController* aController, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void SetUpReadableByteStreamControllerFromUnderlyingSource( + JSContext* aCx, ReadableStream* aStream, + JS::Handle<JSObject*> aUnderlyingSource, + UnderlyingSource& aUnderlyingSourceDict, double aHighWaterMark, + ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadableStream.cpp b/dom/streams/ReadableStream.cpp new file mode 100644 index 0000000000..cc2266cbfb --- /dev/null +++ b/dom/streams/ReadableStream.cpp @@ -0,0 +1,1521 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ReadableStream.h" + +#include "ReadIntoRequest.h" +#include "ReadableStreamPipeTo.h" +#include "ReadableStreamTee.h" +#include "StreamUtils.h" +#include "TeeState.h" +#include "js/Array.h" +#include "js/Exception.h" +#include "js/PropertyAndElement.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "js/Iterator.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/BindingCallContext.h" +#include "mozilla/dom/ByteStreamHelpers.h" +#include "mozilla/dom/QueueWithSizes.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/ReadRequest.h" +#include "mozilla/dom/ReadableByteStreamController.h" +#include "mozilla/dom/ReadableStreamBYOBReader.h" +#include "mozilla/dom/ReadableStreamBYOBRequest.h" +#include "mozilla/dom/ReadableStreamBinding.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/UnderlyingSourceBinding.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "mozilla/dom/WritableStream.h" +#include "mozilla/dom/WritableStreamDefaultWriter.h" +#include "nsCOMPtr.h" + +#include "mozilla/dom/Promise-inl.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + mozilla::Variant<mozilla::Nothing, + RefPtr<mozilla::dom::ReadableStreamDefaultReader>>& + aReader, + const char* aName, uint32_t aFlags = 0) { + if (aReader.is<RefPtr<mozilla::dom::ReadableStreamDefaultReader>>()) { + ImplCycleCollectionTraverse( + aCallback, + aReader.as<RefPtr<mozilla::dom::ReadableStreamDefaultReader>>(), aName, + aFlags); + } +} + +inline void ImplCycleCollectionUnlink( + mozilla::Variant<mozilla::Nothing, + RefPtr<mozilla::dom::ReadableStreamDefaultReader>>& + aReader) { + aReader = AsVariant(mozilla::Nothing()); +} + +namespace mozilla::dom { + +using namespace streams_abstract; + +// Only needed for refcounted objects. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS( + ReadableStream, (mGlobal, mController, mReader), (mStoredError)) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadableStream) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadableStream) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStream) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ReadableStream::ReadableStream(nsIGlobalObject* aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller) + : mGlobal(aGlobal), mReader(nullptr), mHoldDropCaller(aHoldDropCaller) { + if (mHoldDropCaller == HoldDropJSObjectsCaller::Implicit) { + mozilla::HoldJSObjects(this); + } +} + +ReadableStream::ReadableStream(const GlobalObject& aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller) + : mGlobal(do_QueryInterface(aGlobal.GetAsSupports())), + mReader(nullptr), + mHoldDropCaller(aHoldDropCaller) { + if (mHoldDropCaller == HoldDropJSObjectsCaller::Implicit) { + mozilla::HoldJSObjects(this); + } +} + +ReadableStream::~ReadableStream() { + if (mHoldDropCaller == HoldDropJSObjectsCaller::Implicit) { + mozilla::DropJSObjects(this); + } +} + +JSObject* ReadableStream::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ReadableStream_Binding::Wrap(aCx, this, aGivenProto); +} + +ReadableStreamDefaultReader* ReadableStream::GetDefaultReader() { + return mReader->AsDefault(); +} + +void ReadableStream::SetReader(ReadableStreamGenericReader* aReader) { + mReader = aReader; +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-stream-has-byob-reader +bool ReadableStreamHasBYOBReader(ReadableStream* aStream) { + // Step 1. Let reader be stream.[[reader]]. + ReadableStreamGenericReader* reader = aStream->GetReader(); + + // Step 2. If reader is undefined, return false. + if (!reader) { + return false; + } + + // Step 3. If reader implements ReadableStreamBYOBReader, return true. + // Step 4. Return false. + return reader->IsBYOB(); +} + +// https://streams.spec.whatwg.org/#readable-stream-has-default-reader +bool ReadableStreamHasDefaultReader(ReadableStream* aStream) { + // Step 1. Let reader be stream.[[reader]]. + ReadableStreamGenericReader* reader = aStream->GetReader(); + + // Step 2. If reader is undefined, return false. + if (!reader) { + return false; + } + + // Step 3. If reader implements ReadableStreamDefaultReader, return true. + // Step 4. Return false. + return reader->IsDefault(); +} + +} // namespace streams_abstract + +// Streams Spec: 4.2.4: https://streams.spec.whatwg.org/#rs-prototype +/* static */ +already_AddRefed<ReadableStream> ReadableStream::Constructor( + const GlobalObject& aGlobal, + const Optional<JS::Handle<JSObject*>>& aUnderlyingSource, + const QueuingStrategy& aStrategy, ErrorResult& aRv) { + // Step 1. + JS::Rooted<JSObject*> underlyingSourceObj( + aGlobal.Context(), + aUnderlyingSource.WasPassed() ? aUnderlyingSource.Value() : nullptr); + + // Step 2. + RootedDictionary<UnderlyingSource> underlyingSourceDict(aGlobal.Context()); + if (underlyingSourceObj) { + JS::Rooted<JS::Value> objValue(aGlobal.Context(), + JS::ObjectValue(*underlyingSourceObj)); + dom::BindingCallContext callCx(aGlobal.Context(), + "ReadableStream.constructor"); + aRv.MightThrowJSException(); + if (!underlyingSourceDict.Init(callCx, objValue)) { + aRv.StealExceptionFromJSContext(aGlobal.Context()); + return nullptr; + } + } + + // Step 3. + RefPtr<ReadableStream> readableStream = + new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + + // Step 4. + if (underlyingSourceDict.mType.WasPassed()) { + // Implicit assertion on above check. + MOZ_ASSERT(underlyingSourceDict.mType.Value() == ReadableStreamType::Bytes); + + // Step 4.1 + if (aStrategy.mSize.WasPassed()) { + aRv.ThrowRangeError("Implementation preserved member 'size'"); + return nullptr; + } + + // Step 4.2 + double highWaterMark = ExtractHighWaterMark(aStrategy, 0, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 4.3 + SetUpReadableByteStreamControllerFromUnderlyingSource( + aGlobal.Context(), readableStream, underlyingSourceObj, + underlyingSourceDict, highWaterMark, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return readableStream.forget(); + } + + // Step 5.1 (implicit in above check) + // Step 5.2. Extract callback. + // + // Implementation Note: The specification demands that if the size doesn't + // exist, we instead would provide an algorithm that returns 1. Instead, we + // will teach callers that a missing callback should simply return 1, rather + // than gin up a fake callback here. + // + // This decision may need to be revisited if the default action ever diverges + // within the specification. + RefPtr<QueuingStrategySize> sizeAlgorithm = + aStrategy.mSize.WasPassed() ? &aStrategy.mSize.Value() : nullptr; + + // Step 5.3 + double highWaterMark = ExtractHighWaterMark(aStrategy, 1, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 5.4. + SetupReadableStreamDefaultControllerFromUnderlyingSource( + aGlobal.Context(), readableStream, underlyingSourceObj, + underlyingSourceDict, highWaterMark, sizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return readableStream.forget(); +} + +// https://streams.spec.whatwg.org/#readable-stream-from-iterable +class ReadableStreamFromAlgorithms final + : public UnderlyingSourceAlgorithmsWrapper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + ReadableStreamFromAlgorithms, UnderlyingSourceAlgorithmsWrapper) + + ReadableStreamFromAlgorithms(nsIGlobalObject* aGlobal, + JS::Handle<JSObject*> aIteratorRecord) + : mGlobal(aGlobal), mIteratorRecordMaybeCrossRealm(aIteratorRecord) { + mozilla::HoldJSObjects(this); + }; + + // Step 3. Let startAlgorithm be an algorithm that returns undefined. + // Note: Provided by UnderlyingSourceAlgorithmsWrapper::StartCallback. + + // Step 4. Let pullAlgorithm be the following steps: + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PullCallbackImpl( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) override { + aRv.MightThrowJSException(); + + JS::Rooted<JSObject*> iteratorRecord(aCx, mIteratorRecordMaybeCrossRealm); + JSAutoRealm ar(aCx, iteratorRecord); + + // Step 1. Let nextResult be IteratorNext(iteratorRecord). + JS::Rooted<JS::Value> nextResult(aCx); + if (!JS::IteratorNext(aCx, iteratorRecord, &nextResult)) { + // Step 2. If nextResult is an abrupt completion, return a promise + // rejected with nextResult.[[Value]]. + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 3. Let nextPromise be a promise resolved with nextResult.[[Value]]. + RefPtr<Promise> nextPromise = Promise::CreateInfallible(mGlobal); + nextPromise->MaybeResolve(nextResult); + + // Step 4. Return the result of reacting to nextPromise with the following + // fulfillment steps, given iterResult: + auto result = nextPromise->ThenWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aIterResult, ErrorResult& aRv, + const RefPtr<ReadableStreamDefaultController>& aController) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> already_AddRefed<Promise> { + aRv.MightThrowJSException(); + + // Step 4.1. If Type(iterResult) is not Object, throw a TypeError. + if (!aIterResult.isObject()) { + aRv.ThrowTypeError("next() returned a non-object value"); + return nullptr; + } + + JS::Rooted<JSObject*> iterResult(aCx, &aIterResult.toObject()); + + // Step 4.2. Let done be ? IteratorComplete(iterResult). + bool done = false; + if (!JS::IteratorComplete(aCx, iterResult, &done)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 4.3. If done is true: + if (done) { + // Step 4.3.1. Perform ! + // ReadableStreamDefaultControllerClose(stream.[[controller]]). + ReadableStreamDefaultControllerClose(aCx, aController, aRv); + } else { + // Step 4.4. Otherwise: + // Step 4.4.1. Let value be ? IteratorValue(iterResult). + JS::Rooted<JS::Value> value(aCx); + if (!JS::IteratorValue(aCx, iterResult, &value)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 4.4.2. Perform ! + // ReadableStreamDefaultControllerEnqueue(stream.[[controller]], + // value). + ReadableStreamDefaultControllerEnqueue(aCx, aController, value, + aRv); + } + + return nullptr; + }, + RefPtr(aController.AsDefault())); + if (result.isErr()) { + aRv.Throw(result.unwrapErr()); + return nullptr; + } + return result.unwrap().forget(); + }; + + // Step 5. Let cancelAlgorithm be the following steps, given reason: + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelCallbackImpl( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override { + aRv.MightThrowJSException(); + + JS::Rooted<JSObject*> iteratorRecord(aCx, mIteratorRecordMaybeCrossRealm); + JSAutoRealm ar(aCx, iteratorRecord); + + // Step 1. Let iterator be iteratorRecord.[[Iterator]]. + JS::Rooted<JS::Value> iterator(aCx); + if (!JS::GetIteratorRecordIterator(aCx, iteratorRecord, &iterator)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 2. Let returnMethod be GetMethod(iterator, "return"). + JS::Rooted<JS::Value> returnMethod(aCx); + if (!JS::GetReturnMethod(aCx, iterator, &returnMethod)) { + // Step 3. If returnMethod is an abrupt completion, return a promise + // rejected with returnMethod.[[Value]]. + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 4. If returnMethod.[[Value]] is undefined, return a promise resolved + // with undefined. + if (returnMethod.isUndefined()) { + return Promise::CreateResolvedWithUndefined(mGlobal, aRv); + } + + // Step 5. Let returnResult be Call(returnMethod.[[Value]], iterator, « + // reason »). + JS::Rooted<JS::Value> returnResult(aCx); + if (!JS::Call(aCx, iterator, returnMethod, + JS::HandleValueArray(aReason.Value()), &returnResult)) { + // Step 6. If returnResult is an abrupt completion, return a promise + // rejected with returnResult.[[Value]]. + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 7. Let returnPromise be a promise resolved with + // returnResult.[[Value]]. + RefPtr<Promise> returnPromise = Promise::CreateInfallible(mGlobal); + returnPromise->MaybeResolve(returnResult); + + // Step 8. Return the result of reacting to returnPromise with the following + // fulfillment steps, given iterResult: + auto result = returnPromise->ThenWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aIterResult, ErrorResult& aRv) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> already_AddRefed<Promise> { + // Step 8.1. If Type(iterResult) is not Object, throw a TypeError. + if (!aIterResult.isObject()) { + aRv.ThrowTypeError("return() returned a non-object value"); + return nullptr; + } + + // Step 8.2. Return undefined. + return nullptr; + }); + if (result.isErr()) { + aRv.Throw(result.unwrapErr()); + return nullptr; + } + return result.unwrap().forget(); + }; + + protected: + ~ReadableStreamFromAlgorithms() override { mozilla::DropJSObjects(this); }; + + private: + // Virtually const, but are cycle collected + nsCOMPtr<nsIGlobalObject> mGlobal; + JS::Heap<JSObject*> mIteratorRecordMaybeCrossRealm; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED_WITH_JS_MEMBERS( + ReadableStreamFromAlgorithms, UnderlyingSourceAlgorithmsWrapper, (mGlobal), + (mIteratorRecordMaybeCrossRealm)) +NS_IMPL_ADDREF_INHERITED(ReadableStreamFromAlgorithms, + UnderlyingSourceAlgorithmsWrapper) +NS_IMPL_RELEASE_INHERITED(ReadableStreamFromAlgorithms, + UnderlyingSourceAlgorithmsWrapper) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamFromAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsWrapper) + +// https://streams.spec.whatwg.org/#readable-stream-from-iterable +static already_AddRefed<ReadableStream> MOZ_CAN_RUN_SCRIPT +ReadableStreamFromIterable(JSContext* aCx, nsIGlobalObject* aGlobal, + JS::Handle<JS::Value> aAsyncIterable, + ErrorResult& aRv) { + aRv.MightThrowJSException(); + + // Step 1. Let stream be undefined. (not required) + // Step 2. Let iteratorRecord be ? GetIterator(asyncIterable, async). + JS::Rooted<JSObject*> iteratorRecord( + aCx, JS::GetIteratorObject(aCx, aAsyncIterable, true)); + if (!iteratorRecord) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Steps 3-5. are in ReadableStreamFromAlgorithms. + auto algorithms = + MakeRefPtr<ReadableStreamFromAlgorithms>(aGlobal, iteratorRecord); + + // Step 6. Set stream to ! CreateReadableStream(startAlgorithm, pullAlgorithm, + // cancelAlgorithm, 0). + // Step 7. Return stream. + return ReadableStream::CreateAbstract(aCx, aGlobal, algorithms, + mozilla::Some(0.0), nullptr, aRv); +} + +/* static */ +already_AddRefed<ReadableStream> ReadableStream::From( + const GlobalObject& aGlobal, JS::Handle<JS::Value> aAsyncIterable, + ErrorResult& aRv) { + // Step 1. Return ? ReadableStreamFromIterable(asyncIterable). + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + return ReadableStreamFromIterable(aGlobal.Context(), global, aAsyncIterable, + aRv); +} + +// Dealing with const this ptr is a pain, so just re-implement. +// https://streams.spec.whatwg.org/#is-readable-stream-locked +bool ReadableStream::Locked() const { + // Step 1 + 2. + return mReader; +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#initialize-readable-stream +static void InitializeReadableStream(ReadableStream* aStream) { + // Step 1. + aStream->SetState(ReadableStream::ReaderState::Readable); + + // Step 2. + aStream->SetReader(nullptr); + aStream->SetStoredError(JS::UndefinedHandleValue); + + // Step 3. + aStream->SetDisturbed(false); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#create-readable-stream +MOZ_CAN_RUN_SCRIPT +already_AddRefed<ReadableStream> ReadableStream::CreateAbstract( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsBase* aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, QueuingStrategySize* aSizeAlgorithm, + ErrorResult& aRv) { + // Step 1. If highWaterMark was not passed, set it to 1. + double highWaterMark = aHighWaterMark.valueOr(1.0); + + // Step 2. consumers of sizeAlgorithm + // handle null algorithms correctly. + // Step 3. + MOZ_ASSERT(IsNonNegativeNumber(highWaterMark)); + // Step 4. + RefPtr<ReadableStream> stream = + new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + + // Step 5. + InitializeReadableStream(stream); + + // Step 6. + RefPtr<ReadableStreamDefaultController> controller = + new ReadableStreamDefaultController(aGlobal); + + // Step 7. + SetUpReadableStreamDefaultController(aCx, stream, controller, aAlgorithms, + highWaterMark, aSizeAlgorithm, aRv); + + // Step 8. + return stream.forget(); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-stream-close +void ReadableStreamClose(JSContext* aCx, ReadableStream* aStream, + ErrorResult& aRv) { + // Step 1. + MOZ_ASSERT(aStream->State() == ReadableStream::ReaderState::Readable); + + // Step 2. + aStream->SetState(ReadableStream::ReaderState::Closed); + + // Step 3. + ReadableStreamGenericReader* reader = aStream->GetReader(); + + // Step 4. + if (!reader) { + return; + } + + // Step 5. + reader->ClosedPromise()->MaybeResolveWithUndefined(); + + // Step 6. + if (reader->IsDefault()) { + // Step 6.1. Let readRequests be reader.[[readRequests]]. + // Move LinkedList out of DefaultReader onto stack to avoid the potential + // for concurrent modification, which could invalidate the iterator. + // + // See https://bugs.chromium.org/p/chromium/issues/detail?id=1045874 as an + // example of the kind of issue that could occur. + LinkedList<RefPtr<ReadRequest>> readRequests = + std::move(reader->AsDefault()->ReadRequests()); + + // Step 6.2. Set reader.[[readRequests]] to an empty list. + // Note: The std::move already cleared this anyway. + reader->AsDefault()->ReadRequests().clear(); + + // Step 6.3. For each readRequest of readRequests, + // Drain the local list and destroy elements along the way. + while (RefPtr<ReadRequest> readRequest = readRequests.popFirst()) { + // Step 6.3.1. Perform readRequest’s close steps. + readRequest->CloseSteps(aCx, aRv); + if (aRv.Failed()) { + return; + } + } + } +} + +// https://streams.spec.whatwg.org/#readable-stream-cancel +already_AddRefed<Promise> ReadableStreamCancel(JSContext* aCx, + ReadableStream* aStream, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1. + aStream->SetDisturbed(true); + + // Step 2. + if (aStream->State() == ReadableStream::ReaderState::Closed) { + RefPtr<Promise> promise = + Promise::CreateInfallible(aStream->GetParentObject()); + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // Step 3. + if (aStream->State() == ReadableStream::ReaderState::Errored) { + JS::Rooted<JS::Value> storedError(aCx, aStream->StoredError()); + return Promise::CreateRejected(aStream->GetParentObject(), storedError, + aRv); + } + + // Step 4. + ReadableStreamClose(aCx, aStream, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 5. + ReadableStreamGenericReader* reader = aStream->GetReader(); + + // Step 6. + if (reader && reader->IsBYOB()) { + // Step 6.1. Let readIntoRequests be reader.[[readIntoRequests]]. + LinkedList<RefPtr<ReadIntoRequest>> readIntoRequests = + std::move(reader->AsBYOB()->ReadIntoRequests()); + + // Step 6.2. Set reader.[[readIntoRequests]] to an empty list. + // Note: The std::move already cleared this anyway. + reader->AsBYOB()->ReadIntoRequests().clear(); + + // Step 6.3. For each readIntoRequest of readIntoRequests, + while (RefPtr<ReadIntoRequest> readIntoRequest = + readIntoRequests.popFirst()) { + // Step 6.3.1.Perform readIntoRequest’s close steps, given undefined. + readIntoRequest->CloseSteps(aCx, JS::UndefinedHandleValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + } + + // Step 7. + RefPtr<ReadableStreamController> controller(aStream->Controller()); + RefPtr<Promise> sourceCancelPromise = + controller->CancelSteps(aCx, aError, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 8. + RefPtr<Promise> promise = + Promise::CreateInfallible(sourceCancelPromise->GetParentObject()); + + // ThenWithCycleCollectedArgs will carry promise, keeping it alive until the + // callback executes. + Result<RefPtr<Promise>, nsresult> returnResult = + sourceCancelPromise->ThenWithCycleCollectedArgs( + [](JSContext*, JS::Handle<JS::Value>, ErrorResult&, + RefPtr<Promise> newPromise) { + newPromise->MaybeResolveWithUndefined(); + return newPromise.forget(); + }, + promise); + + if (returnResult.isErr()) { + aRv.Throw(returnResult.unwrapErr()); + return nullptr; + } + + return returnResult.unwrap().forget(); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-cancel +already_AddRefed<Promise> ReadableStream::Cancel(JSContext* aCx, + JS::Handle<JS::Value> aReason, + ErrorResult& aRv) { + // Step 1. If ! IsReadableStreamLocked(this) is true, + // return a promise rejected with a TypeError exception. + if (Locked()) { + aRv.ThrowTypeError("Cannot cancel a stream locked by a reader."); + return nullptr; + } + + // Step 2. Return ! ReadableStreamCancel(this, reason). + RefPtr<ReadableStream> thisRefPtr = this; + return ReadableStreamCancel(aCx, thisRefPtr, aReason, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#acquire-readable-stream-reader +already_AddRefed<ReadableStreamDefaultReader> +AcquireReadableStreamDefaultReader(ReadableStream* aStream, ErrorResult& aRv) { + // Step 1. + RefPtr<ReadableStreamDefaultReader> reader = + new ReadableStreamDefaultReader(aStream->GetParentObject()); + + // Step 2. + SetUpReadableStreamDefaultReader(reader, aStream, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 3. + return reader.forget(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-get-reader +void ReadableStream::GetReader(const ReadableStreamGetReaderOptions& aOptions, + OwningReadableStreamReader& resultReader, + ErrorResult& aRv) { + // Step 1. If options["mode"] does not exist, + // return ? AcquireReadableStreamDefaultReader(this). + if (!aOptions.mMode.WasPassed()) { + RefPtr<ReadableStreamDefaultReader> defaultReader = + AcquireReadableStreamDefaultReader(this, aRv); + if (aRv.Failed()) { + return; + } + resultReader.SetAsReadableStreamDefaultReader() = defaultReader; + return; + } + + // Step 2. Assert: options["mode"] is "byob". + MOZ_ASSERT(aOptions.mMode.Value() == ReadableStreamReaderMode::Byob); + + // Step 3. Return ? AcquireReadableStreamBYOBReader(this). + RefPtr<ReadableStreamBYOBReader> byobReader = + AcquireReadableStreamBYOBReader(this, aRv); + if (aRv.Failed()) { + return; + } + resultReader.SetAsReadableStreamBYOBReader() = byobReader; +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#is-readable-stream-locked +bool IsReadableStreamLocked(ReadableStream* aStream) { + // Step 1 + 2. + return aStream->Locked(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-pipe-through +MOZ_CAN_RUN_SCRIPT already_AddRefed<ReadableStream> ReadableStream::PipeThrough( + const ReadableWritablePair& aTransform, const StreamPipeOptions& aOptions, + ErrorResult& aRv) { + // Step 1: If ! IsReadableStreamLocked(this) is true, throw a TypeError + // exception. + if (IsReadableStreamLocked(this)) { + aRv.ThrowTypeError("Cannot pipe from a locked stream."); + return nullptr; + } + + // Step 2: If ! IsWritableStreamLocked(transform["writable"]) is true, throw a + // TypeError exception. + if (IsWritableStreamLocked(aTransform.mWritable)) { + aRv.ThrowTypeError("Cannot pipe to a locked stream."); + return nullptr; + } + + // Step 3: Let signal be options["signal"] if it exists, or undefined + // otherwise. + RefPtr<AbortSignal> signal = + aOptions.mSignal.WasPassed() ? &aOptions.mSignal.Value() : nullptr; + + // Step 4: Let promise be ! ReadableStreamPipeTo(this, transform["writable"], + // options["preventClose"], options["preventAbort"], options["preventCancel"], + // signal). + RefPtr<WritableStream> writable = aTransform.mWritable; + RefPtr<Promise> promise = ReadableStreamPipeTo( + this, writable, aOptions.mPreventClose, aOptions.mPreventAbort, + aOptions.mPreventCancel, signal, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 5: Set promise.[[PromiseIsHandled]] to true. + MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled()); + + // Step 6: Return transform["readable"]. + return do_AddRef(aTransform.mReadable.get()); +}; + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-stream-get-num-read-requests +double ReadableStreamGetNumReadRequests(ReadableStream* aStream) { + // Step 1. + MOZ_ASSERT(ReadableStreamHasDefaultReader(aStream)); + + // Step 2. + return double(aStream->GetDefaultReader()->ReadRequests().length()); +} + +// https://streams.spec.whatwg.org/#readable-stream-error +void ReadableStreamError(JSContext* aCx, ReadableStream* aStream, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + // Step 1. + MOZ_ASSERT(aStream->State() == ReadableStream::ReaderState::Readable); + + // Step 2. + aStream->SetState(ReadableStream::ReaderState::Errored); + + // Step 3. + aStream->SetStoredError(aValue); + + // Step 4. + ReadableStreamGenericReader* reader = aStream->GetReader(); + + // Step 5. + if (!reader) { + return; + } + + // Step 6. + reader->ClosedPromise()->MaybeReject(aValue); + + // Step 7. + reader->ClosedPromise()->SetSettledPromiseIsHandled(); + + // Step 8. + if (reader->IsDefault()) { + // Step 8.1. Perform ! ReadableStreamDefaultReaderErrorReadRequests(reader, + // e). + RefPtr<ReadableStreamDefaultReader> defaultReader = reader->AsDefault(); + ReadableStreamDefaultReaderErrorReadRequests(aCx, defaultReader, aValue, + aRv); + if (aRv.Failed()) { + return; + } + } else { + // Step 9. Otherwise, + // Step 9.1. Assert: reader implements ReadableStreamBYOBReader. + MOZ_ASSERT(reader->IsBYOB()); + + // Step 9.2. Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, + // e). + RefPtr<ReadableStreamBYOBReader> byobReader = reader->AsBYOB(); + ReadableStreamBYOBReaderErrorReadIntoRequests(aCx, byobReader, aValue, aRv); + if (aRv.Failed()) { + return; + } + } +} + +// https://streams.spec.whatwg.org/#rs-default-controller-close +void ReadableStreamFulfillReadRequest(JSContext* aCx, ReadableStream* aStream, + JS::Handle<JS::Value> aChunk, bool aDone, + ErrorResult& aRv) { + // Step 1. + MOZ_ASSERT(ReadableStreamHasDefaultReader(aStream)); + + // Step 2. + ReadableStreamDefaultReader* reader = aStream->GetDefaultReader(); + + // Step 3. + MOZ_ASSERT(!reader->ReadRequests().isEmpty()); + + // Step 4+5. + RefPtr<ReadRequest> readRequest = reader->ReadRequests().popFirst(); + + // Step 6. + if (aDone) { + readRequest->CloseSteps(aCx, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 7. + readRequest->ChunkSteps(aCx, aChunk, aRv); +} + +// https://streams.spec.whatwg.org/#readable-stream-add-read-request +void ReadableStreamAddReadRequest(ReadableStream* aStream, + ReadRequest* aReadRequest) { + // Step 1. + MOZ_ASSERT(aStream->GetReader()->IsDefault()); + // Step 2. + MOZ_ASSERT(aStream->State() == ReadableStream::ReaderState::Readable); + // Step 3. + aStream->GetDefaultReader()->ReadRequests().insertBack(aReadRequest); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee +// Step 14, 15 +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> +ReadableStreamDefaultTeeSourceAlgorithms::CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + // Step 1. + mTeeState->SetCanceled(mBranch, true); + + // Step 2. + mTeeState->SetReason(mBranch, aReason.Value()); + + // Step 3. + + if (mTeeState->Canceled(OtherTeeBranch(mBranch))) { + // Step 3.1 + + JS::Rooted<JSObject*> compositeReason(aCx, JS::NewArrayObject(aCx, 2)); + if (!compositeReason) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + JS::Rooted<JS::Value> reason1(aCx, mTeeState->Reason1()); + if (!JS_SetElement(aCx, compositeReason, 0, reason1)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + JS::Rooted<JS::Value> reason2(aCx, mTeeState->Reason2()); + if (!JS_SetElement(aCx, compositeReason, 1, reason2)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 3.2 + JS::Rooted<JS::Value> compositeReasonValue( + aCx, JS::ObjectValue(*compositeReason)); + RefPtr<ReadableStream> stream(mTeeState->GetStream()); + RefPtr<Promise> cancelResult = + ReadableStreamCancel(aCx, stream, compositeReasonValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 3.3 + mTeeState->CancelPromise()->MaybeResolve(cancelResult); + } + + // Step 4. + return do_AddRef(mTeeState->CancelPromise()); +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee +MOZ_CAN_RUN_SCRIPT +static void ReadableStreamDefaultTee(JSContext* aCx, ReadableStream* aStream, + bool aCloneForBranch2, + nsTArray<RefPtr<ReadableStream>>& aResult, + ErrorResult& aRv) { + // Step 1. Implicit. + // Step 2. Implicit. + + // Steps 3-12 are contained in the construction of Tee State. + RefPtr<TeeState> teeState = TeeState::Create(aStream, aCloneForBranch2, aRv); + if (aRv.Failed()) { + return; + } + + // Step 13 - 16 + auto branch1Algorithms = MakeRefPtr<ReadableStreamDefaultTeeSourceAlgorithms>( + teeState, TeeBranch::Branch1); + auto branch2Algorithms = MakeRefPtr<ReadableStreamDefaultTeeSourceAlgorithms>( + teeState, TeeBranch::Branch2); + + // Step 17. + nsCOMPtr<nsIGlobalObject> global( + do_AddRef(teeState->GetStream()->GetParentObject())); + teeState->SetBranch1(ReadableStream::CreateAbstract( + aCx, global, branch1Algorithms, mozilla::Nothing(), nullptr, aRv)); + if (aRv.Failed()) { + return; + } + + // Step 18. + teeState->SetBranch2(ReadableStream::CreateAbstract( + aCx, global, branch2Algorithms, mozilla::Nothing(), nullptr, aRv)); + if (aRv.Failed()) { + return; + } + + // Step 19. + teeState->GetReader()->ClosedPromise()->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + TeeState* aTeeState) {}, + [](JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv, + TeeState* aTeeState) { + // Step 19.1. + ReadableStreamDefaultControllerError( + aCx, aTeeState->Branch1()->DefaultController(), aReason, aRv); + if (aRv.Failed()) { + return; + } + + // Step 19.2 + ReadableStreamDefaultControllerError( + aCx, aTeeState->Branch2()->DefaultController(), aReason, aRv); + if (aRv.Failed()) { + return; + } + + // Step 19.3 + if (!aTeeState->Canceled1() || !aTeeState->Canceled2()) { + aTeeState->CancelPromise()->MaybeResolveWithUndefined(); + } + }, + RefPtr(teeState)); + + // Step 20. + aResult.AppendElement(teeState->Branch1()); + aResult.AppendElement(teeState->Branch2()); +} + +// https://streams.spec.whatwg.org/#rs-pipe-to +already_AddRefed<Promise> ReadableStream::PipeTo( + WritableStream& aDestination, const StreamPipeOptions& aOptions, + ErrorResult& aRv) { + // Step 1. If !IsReadableStreamLocked(this) is true, return a promise rejected + // with a TypeError exception. + if (IsReadableStreamLocked(this)) { + aRv.ThrowTypeError("Cannot pipe from a locked stream."); + return nullptr; + } + + // Step 2. If !IsWritableStreamLocked(destination) is true, return a promise + // rejected with a TypeError exception. + if (IsWritableStreamLocked(&aDestination)) { + aRv.ThrowTypeError("Cannot pipe to a locked stream."); + return nullptr; + } + + // Step 3. Let signal be options["signal"] if it exists, or undefined + // otherwise. + RefPtr<AbortSignal> signal = + aOptions.mSignal.WasPassed() ? &aOptions.mSignal.Value() : nullptr; + + // Step 4. Return ! ReadableStreamPipeTo(this, destination, + // options["preventClose"], options["preventAbort"], options["preventCancel"], + // signal). + return ReadableStreamPipeTo(this, &aDestination, aOptions.mPreventClose, + aOptions.mPreventAbort, aOptions.mPreventCancel, + signal, aRv); +} + +// https://streams.spec.whatwg.org/#readable-stream-tee +MOZ_CAN_RUN_SCRIPT +static void ReadableStreamTee(JSContext* aCx, ReadableStream* aStream, + bool aCloneForBranch2, + nsTArray<RefPtr<ReadableStream>>& aResult, + ErrorResult& aRv) { + // Step 1. Implicit. + // Step 2. Implicit. + // Step 3. + if (aStream->Controller()->IsByte()) { + ReadableByteStreamTee(aCx, aStream, aResult, aRv); + return; + } + // Step 4. + ReadableStreamDefaultTee(aCx, aStream, aCloneForBranch2, aResult, aRv); +} + +void ReadableStream::Tee(JSContext* aCx, + nsTArray<RefPtr<ReadableStream>>& aResult, + ErrorResult& aRv) { + ReadableStreamTee(aCx, this, false, aResult, aRv); +} + +void ReadableStream::IteratorData::Traverse( + nsCycleCollectionTraversalCallback& cb) { + ReadableStream::IteratorData* tmp = this; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReader); +} +void ReadableStream::IteratorData::Unlink() { + ReadableStream::IteratorData* tmp = this; + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReader); +} + +// https://streams.spec.whatwg.org/#rs-get-iterator +void ReadableStream::InitAsyncIteratorData( + IteratorData& aData, Iterator::IteratorType aType, + const ReadableStreamIteratorOptions& aOptions, ErrorResult& aRv) { + // Step 1. Let reader be ? AcquireReadableStreamDefaultReader(stream). + RefPtr<ReadableStreamDefaultReader> reader = + AcquireReadableStreamDefaultReader(this, aRv); + if (aRv.Failed()) { + return; + } + + // Step 2. Set iterator’s reader to reader. + aData.mReader = reader; + + // Step 3. Let preventCancel be args[0]["preventCancel"]. + // Step 4. Set iterator’s prevent cancel to preventCancel. + aData.mPreventCancel = aOptions.mPreventCancel; +} + +// https://streams.spec.whatwg.org/#rs-asynciterator-prototype-next +// Step 4. +struct IteratorReadRequest : public ReadRequest { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IteratorReadRequest, ReadRequest) + + RefPtr<Promise> mPromise; + RefPtr<ReadableStreamDefaultReader> mReader; + + explicit IteratorReadRequest(Promise* aPromise, + ReadableStreamDefaultReader* aReader) + : mPromise(aPromise), mReader(aReader) {} + + // chunk steps, given chunk + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + // Step 1. Resolve promise with chunk. + mPromise->MaybeResolve(aChunk); + } + + // close steps + void CloseSteps(JSContext* aCx, ErrorResult& aRv) override { + // Step 1. Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReaderRelease(aCx, mReader, aRv); + if (aRv.Failed()) { + mPromise->MaybeRejectWithUndefined(); + return; + } + + // Step 2. Resolve promise with end of iteration. + iterator_utils::ResolvePromiseForFinished(mPromise); + } + + // error steps, given e + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) override { + // Step 1. Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReaderRelease(aCx, mReader, aRv); + if (aRv.Failed()) { + mPromise->MaybeRejectWithUndefined(); + return; + } + + // Step 2. Reject promise with e. + mPromise->MaybeReject(aError); + } + + protected: + virtual ~IteratorReadRequest() = default; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(IteratorReadRequest, ReadRequest, mPromise, + mReader) + +NS_IMPL_ADDREF_INHERITED(IteratorReadRequest, ReadRequest) +NS_IMPL_RELEASE_INHERITED(IteratorReadRequest, ReadRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IteratorReadRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadRequest) + +// https://streams.spec.whatwg.org/#rs-asynciterator-prototype-next +already_AddRefed<Promise> ReadableStream::GetNextIterationResult( + Iterator* aIterator, ErrorResult& aRv) { + // Step 1. Let reader be iterator’s reader. + RefPtr<ReadableStreamDefaultReader> reader = aIterator->Data().mReader; + + // Step 2. Assert: reader.[[stream]] is not undefined. + MOZ_ASSERT(reader->GetStream()); + + // Step 3. Let promise be a new promise. + RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject()); + + // Step 4. Let readRequest be a new read request with the following items: + RefPtr<ReadRequest> request = new IteratorReadRequest(promise, reader); + + // Step 5. Perform ! ReadableStreamDefaultReaderRead(this, readRequest). + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + aRv.ThrowUnknownError("Internal error"); + return nullptr; + } + + ReadableStreamDefaultReaderRead(jsapi.cx(), reader, request, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 6. Return promise. + return promise.forget(); +} + +// https://streams.spec.whatwg.org/#rs-asynciterator-prototype-return +already_AddRefed<Promise> ReadableStream::IteratorReturn( + JSContext* aCx, Iterator* aIterator, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + // Step 1. Let reader be iterator’s reader. + RefPtr<ReadableStreamDefaultReader> reader = aIterator->Data().mReader; + + // Step 2. Assert: reader.[[stream]] is not undefined. + MOZ_ASSERT(reader->GetStream()); + + // Step 3. Assert: reader.[[readRequests]] is empty, as the async iterator + // machinery guarantees that any previous calls to next() have settled before + // this is called. + MOZ_ASSERT(reader->ReadRequests().isEmpty()); + + // Step 4. If iterator’s prevent cancel is false: + if (!aIterator->Data().mPreventCancel) { + // Step 4.1. Let result be ! ReadableStreamReaderGenericCancel(reader, arg). + RefPtr<ReadableStream> stream(reader->GetStream()); + RefPtr<Promise> result = ReadableStreamCancel(aCx, stream, aValue, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + MOZ_DIAGNOSTIC_ASSERT( + reader->GetStream(), + "We shouldn't have a null stream here (bug 1821169)."); + if (!reader->GetStream()) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Step 4.2. Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReaderRelease(aCx, reader, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // Step 4.3. Return result. + return result.forget(); + } + + // Step 5. Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReaderRelease(aCx, reader, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // Step 6. Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(GetParentObject(), aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-stream-add-read-into-request +void ReadableStreamAddReadIntoRequest(ReadableStream* aStream, + ReadIntoRequest* aReadIntoRequest) { + // Step 1. Assert: stream.[[reader]] implements ReadableStreamBYOBReader. + MOZ_ASSERT(aStream->GetReader()->IsBYOB()); + + // Step 2. Assert: stream.[[state]] is "readable" or "closed". + MOZ_ASSERT(aStream->State() == ReadableStream::ReaderState::Readable || + aStream->State() == ReadableStream::ReaderState::Closed); + + // Step 3. Append readRequest to stream.[[reader]].[[readIntoRequests]]. + aStream->GetReader()->AsBYOB()->ReadIntoRequests().insertBack( + aReadIntoRequest); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#abstract-opdef-createreadablebytestream +already_AddRefed<ReadableStream> ReadableStream::CreateByteAbstract( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsBase* aAlgorithms, ErrorResult& aRv) { + // Step 1. Let stream be a new ReadableStream. + RefPtr<ReadableStream> stream = + new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + + // Step 2. Perform ! InitializeReadableStream(stream). + InitializeReadableStream(stream); + + // Step 3. Let controller be a new ReadableByteStreamController. + RefPtr<ReadableByteStreamController> controller = + new ReadableByteStreamController(aGlobal); + + // Step 4. Perform ? SetUpReadableByteStreamController(stream, controller, + // startAlgorithm, pullAlgorithm, cancelAlgorithm, 0, undefined). + SetUpReadableByteStreamController(aCx, stream, controller, aAlgorithms, 0, + mozilla::Nothing(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Return stream. + return stream.forget(); +} + +// https://streams.spec.whatwg.org/#readablestream-set-up +// (except this instead creates a new ReadableStream rather than accepting an +// existing instance) +// _BOUNDARY because `aAlgorithms->StartCallback` (called by +// SetUpReadableStreamDefaultController below) should not be able to run script +// in this case. +MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<ReadableStream> +ReadableStream::CreateNative(JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsWrapper& aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, + ErrorResult& aRv) { + // an optional number highWaterMark (default 1) + double highWaterMark = aHighWaterMark.valueOr(1); + // and if given, highWaterMark must be a non-negative, non-NaN number. + MOZ_ASSERT(IsNonNegativeNumber(highWaterMark)); + + // Step 1: Let startAlgorithm be an algorithm that returns undefined. + // Step 2: Let pullAlgorithmWrapper be an algorithm that runs these steps: + // Step 3: Let cancelAlgorithmWrapper be an algorithm that runs these steps: + // (Done by UnderlyingSourceAlgorithmsWrapper) + + // Step 4: If sizeAlgorithm was not given, then set it to an algorithm that + // returns 1. (Callers will treat nullptr as such, see + // ReadableStream::Constructor for details) + + // Step 5: Perform ! InitializeReadableStream(stream). + RefPtr<ReadableStream> stream = + new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + + // Step 6: Let controller be a new ReadableStreamDefaultController. + auto controller = MakeRefPtr<ReadableStreamDefaultController>(aGlobal); + + // Step 7: Perform ! SetUpReadableStreamDefaultController(stream, controller, + // startAlgorithm, pullAlgorithmWrapper, cancelAlgorithmWrapper, + // highWaterMark, sizeAlgorithm). + SetUpReadableStreamDefaultController(aCx, stream, controller, &aAlgorithms, + highWaterMark, aSizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + return stream.forget(); +} + +// https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support +// _BOUNDARY because `aAlgorithms->StartCallback` (called by +// SetUpReadableByteStreamController below) should not be able to run script in +// this case. +MOZ_CAN_RUN_SCRIPT_BOUNDARY void ReadableStream::SetUpByteNative( + JSContext* aCx, UnderlyingSourceAlgorithmsWrapper& aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, ErrorResult& aRv) { + // an optional number highWaterMark (default 0) + double highWaterMark = aHighWaterMark.valueOr(0); + // and if given, highWaterMark must be a non-negative, non-NaN number. + MOZ_ASSERT(IsNonNegativeNumber(highWaterMark)); + + // Step 1: Let startAlgorithm be an algorithm that returns undefined. + // Step 2: Let pullAlgorithmWrapper be an algorithm that runs these steps: + // Step 3: Let cancelAlgorithmWrapper be an algorithm that runs these steps: + // (Done by UnderlyingSourceAlgorithmsWrapper) + + // Step 4: Perform ! InitializeReadableStream(stream). + // (Covered by constructor) + + // Step 5: Let controller be a new ReadableByteStreamController. + auto controller = MakeRefPtr<ReadableByteStreamController>(GetParentObject()); + + // Step 6: Perform ! SetUpReadableByteStreamController(stream, controller, + // startAlgorithm, pullAlgorithmWrapper, cancelAlgorithmWrapper, + // highWaterMark, undefined). + SetUpReadableByteStreamController(aCx, this, controller, &aAlgorithms, + highWaterMark, Nothing(), aRv); +} + +already_AddRefed<ReadableStream> ReadableStream::CreateByteNative( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsWrapper& aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, ErrorResult& aRv) { + RefPtr<ReadableStream> stream = + new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + stream->SetUpByteNative(aCx, aAlgorithms, aHighWaterMark, aRv); + if (aRv.Failed()) { + return nullptr; + } + return stream.forget(); +} + +// https://streams.spec.whatwg.org/#readablestream-close +void ReadableStream::CloseNative(JSContext* aCx, ErrorResult& aRv) { + MOZ_ASSERT_IF(mController->GetAlgorithms(), + mController->GetAlgorithms()->IsNative()); + // Step 1: If stream.[[controller]] implements ReadableByteStreamController, + if (mController->IsByte()) { + RefPtr<ReadableByteStreamController> controller = mController->AsByte(); + + // Step 1.1: Perform ! + // ReadableByteStreamControllerClose(stream.[[controller]]). + ReadableByteStreamControllerClose(aCx, controller, aRv); + if (aRv.Failed()) { + return; + } + + // Step 1.2: If stream.[[controller]].[[pendingPullIntos]] is not empty, + // perform ! ReadableByteStreamControllerRespond(stream.[[controller]], 0). + if (!controller->PendingPullIntos().isEmpty()) { + ReadableByteStreamControllerRespond(aCx, controller, 0, aRv); + } + return; + } + + // Step 2: Otherwise, perform ! + // ReadableStreamDefaultControllerClose(stream.[[controller]]). + RefPtr<ReadableStreamDefaultController> controller = mController->AsDefault(); + ReadableStreamDefaultControllerClose(aCx, controller, aRv); +} + +// https://streams.spec.whatwg.org/#readablestream-error +void ReadableStream::ErrorNative(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1: If stream.[[controller]] implements ReadableByteStreamController, + // then perform ! ReadableByteStreamControllerError(stream.[[controller]], e). + if (mController->IsByte()) { + ReadableByteStreamControllerError(mController->AsByte(), aError, aRv); + return; + } + // Step 2: Otherwise, perform ! + // ReadableStreamDefaultControllerError(stream.[[controller]], e). + ReadableStreamDefaultControllerError(aCx, mController->AsDefault(), aError, + aRv); +} + +// https://streams.spec.whatwg.org/#readablestream-current-byob-request-view +static void CurrentBYOBRequestView(JSContext* aCx, + ReadableByteStreamController& aController, + JS::MutableHandle<JSObject*> aRetVal, + ErrorResult& aRv) { + // Step 1. Assert: stream.[[controller]] implements + // ReadableByteStreamController. (implicit) + + // Step 2: Let byobRequest be ! + // ReadableByteStreamControllerGetBYOBRequest(stream.[[controller]]). + RefPtr<ReadableStreamBYOBRequest> byobRequest = + ReadableByteStreamControllerGetBYOBRequest(aCx, &aController, aRv); + // Step 3: If byobRequest is null, then return null. + if (!byobRequest) { + aRetVal.set(nullptr); + return; + } + // Step 4: Return byobRequest.[[view]]. + byobRequest->GetView(aCx, aRetVal); +} + +static bool HasSameBufferView(JSContext* aCx, JS::Handle<JSObject*> aX, + JS::Handle<JSObject*> aY, ErrorResult& aRv) { + bool isShared; + JS::Rooted<JSObject*> viewedBufferX( + aCx, JS_GetArrayBufferViewBuffer(aCx, aX, &isShared)); + if (!viewedBufferX) { + aRv.StealExceptionFromJSContext(aCx); + return false; + } + + JS::Rooted<JSObject*> viewedBufferY( + aCx, JS_GetArrayBufferViewBuffer(aCx, aY, &isShared)); + if (!viewedBufferY) { + aRv.StealExceptionFromJSContext(aCx); + return false; + } + + return viewedBufferX == viewedBufferY; +} + +// https://streams.spec.whatwg.org/#readablestream-enqueue +void ReadableStream::EnqueueNative(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) { + MOZ_ASSERT(mController->GetAlgorithms()->IsNative()); + + // Step 1: If stream.[[controller]] implements + // ReadableStreamDefaultController, + if (mController->IsDefault()) { + // Step 1.1: Perform ! + // ReadableStreamDefaultControllerEnqueue(stream.[[controller]], chunk). + RefPtr<ReadableStreamDefaultController> controller = + mController->AsDefault(); + ReadableStreamDefaultControllerEnqueue(aCx, controller, aChunk, aRv); + return; + } + + // Step 2.1: Assert: stream.[[controller]] implements + // ReadableByteStreamController. + MOZ_ASSERT(mController->IsByte()); + RefPtr<ReadableByteStreamController> controller = mController->AsByte(); + + // Step 2.2: Assert: chunk is an ArrayBufferView. + MOZ_ASSERT(aChunk.isObject() && + JS_IsArrayBufferViewObject(&aChunk.toObject())); + JS::Rooted<JSObject*> chunk(aCx, &aChunk.toObject()); + + // Step 3: Let byobView be the current BYOB request view for stream. + JS::Rooted<JSObject*> byobView(aCx); + CurrentBYOBRequestView(aCx, *controller, &byobView, aRv); + if (aRv.Failed()) { + return; + } + + // Step 4: If byobView is non-null, and chunk.[[ViewedArrayBuffer]] is + // byobView.[[ViewedArrayBuffer]], then: + if (byobView && HasSameBufferView(aCx, chunk, byobView, aRv)) { + // Step 4.1: Assert: chunk.[[ByteOffset]] is byobView.[[ByteOffset]]. + MOZ_ASSERT(JS_GetArrayBufferViewByteOffset(chunk) == + JS_GetArrayBufferViewByteOffset(byobView)); + // Step 4.2: Assert: chunk.[[ByteLength]] ≤ byobView.[[ByteLength]]. + MOZ_ASSERT(JS_GetArrayBufferViewByteLength(chunk) <= + JS_GetArrayBufferViewByteLength(byobView)); + // Step 4.3: Perform ? + // ReadableByteStreamControllerRespond(stream.[[controller]], + // chunk.[[ByteLength]]). + ReadableByteStreamControllerRespond( + aCx, controller, JS_GetArrayBufferViewByteLength(chunk), aRv); + return; + } + + if (aRv.Failed()) { + return; + } + + // Step 5: Otherwise, perform ? + // ReadableByteStreamControllerEnqueue(stream.[[controller]], chunk). + ReadableByteStreamControllerEnqueue(aCx, controller, chunk, aRv); +} + +// https://streams.spec.whatwg.org/#readablestream-current-byob-request-view +void ReadableStream::GetCurrentBYOBRequestView( + JSContext* aCx, JS::MutableHandle<JSObject*> aView, ErrorResult& aRv) { + aView.set(nullptr); + + // Step 1: Assert: stream.[[controller]] implements + // ReadableByteStreamController. + MOZ_ASSERT(mController->IsByte()); + + // Step 2: Let byobRequest be ! + // ReadableByteStreamControllerGetBYOBRequest(stream.[[controller]]). + RefPtr<ReadableStreamBYOBRequest> byobRequest = + mController->AsByte()->GetByobRequest(aCx, aRv); + + // Step 3: If byobRequest is null, then return null. + if (!byobRequest || aRv.Failed()) { + return; + } + + // Step 4: Return byobRequest.[[view]]. + byobRequest->GetView(aCx, aView); +} + +// https://streams.spec.whatwg.org/#readablestream-get-a-reader +// To get a reader for a ReadableStream stream, return ? +// AcquireReadableStreamDefaultReader(stream). The result will be a +// ReadableStreamDefaultReader. +already_AddRefed<mozilla::dom::ReadableStreamDefaultReader> +ReadableStream::GetReader(ErrorResult& aRv) { + return AcquireReadableStreamDefaultReader(this, aRv); +} + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStream.h b/dom/streams/ReadableStream.h new file mode 100644 index 0000000000..ac2d06caf1 --- /dev/null +++ b/dom/streams/ReadableStream.h @@ -0,0 +1,292 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStream_h +#define mozilla_dom_ReadableStream_h + +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IterableIterator.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class Promise; +class ReadableStreamBYOBRequest; +class ReadableStreamDefaultReader; +class ReadableStreamGenericReader; +struct ReadableStreamGetReaderOptions; +struct ReadableStreamIteratorOptions; +struct ReadIntoRequest; +class WritableStream; +struct ReadableWritablePair; +struct StreamPipeOptions; + +using ReadableStreamReader = + ReadableStreamDefaultReaderOrReadableStreamBYOBReader; +using OwningReadableStreamReader = + OwningReadableStreamDefaultReaderOrReadableStreamBYOBReader; +class NativeUnderlyingSource; +class BodyStreamHolder; +class UniqueMessagePortId; +class MessagePort; + +class ReadableStream : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ReadableStream) + + friend class WritableStream; + + protected: + virtual ~ReadableStream(); + + nsCOMPtr<nsIGlobalObject> mGlobal; + + // If one extends ReadableStream with another cycle collectable class, + // calling HoldJSObjects and DropJSObjects should happen using 'this' of + // that extending class. And in that case Explicit should be passed to the + // constructor of ReadableStream so that it doesn't make those calls. + // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1801214. + enum class HoldDropJSObjectsCaller { Implicit, Explicit }; + + explicit ReadableStream(const GlobalObject& aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller); + explicit ReadableStream(nsIGlobalObject* aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller); + + public: + // Abstract algorithms + MOZ_CAN_RUN_SCRIPT static already_AddRefed<ReadableStream> CreateAbstract( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsBase* aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT static already_AddRefed<ReadableStream> CreateByteAbstract( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsBase* aAlgorithms, ErrorResult& aRv); + + // Slot Getter/Setters: + MOZ_KNOWN_LIVE ReadableStreamController* Controller() { return mController; } + ReadableStreamDefaultController* DefaultController() { + MOZ_ASSERT(mController && mController->IsDefault()); + return mController->AsDefault(); + } + void SetController(ReadableStreamController& aController) { + MOZ_ASSERT(!mController); + mController = &aController; + } + + bool Disturbed() const { return mDisturbed; } + void SetDisturbed(bool aDisturbed) { mDisturbed = aDisturbed; } + + ReadableStreamGenericReader* GetReader() { return mReader; } + void SetReader(ReadableStreamGenericReader* aReader); + + ReadableStreamDefaultReader* GetDefaultReader(); + + enum class ReaderState { Readable, Closed, Errored }; + + ReaderState State() const { return mState; } + void SetState(const ReaderState& aState) { mState = aState; } + + JS::Value StoredError() const { return mStoredError; } + void SetStoredError(JS::Handle<JS::Value> aStoredError) { + mStoredError = aStoredError; + } + + nsIInputStream* MaybeGetInputStreamIfUnread() { + MOZ_ASSERT(!Disturbed()); + if (UnderlyingSourceAlgorithmsBase* algorithms = + Controller()->GetAlgorithms()) { + return algorithms->MaybeGetInputStreamIfUnread(); + } + return nullptr; + } + + // [Transferable] + // https://html.spec.whatwg.org/multipage/structured-data.html#transfer-steps + MOZ_CAN_RUN_SCRIPT bool Transfer(JSContext* aCx, + UniqueMessagePortId& aPortId); + MOZ_CAN_RUN_SCRIPT static already_AddRefed<ReadableStream> + ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal, + MessagePort& aPort); + // https://html.spec.whatwg.org/multipage/structured-data.html#transfer-receiving-steps + MOZ_CAN_RUN_SCRIPT static bool ReceiveTransfer( + JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort, + JS::MutableHandle<JSObject*> aReturnObject); + + // Public functions to implement other specs + // https://streams.spec.whatwg.org/#other-specs-rs + + // https://streams.spec.whatwg.org/#readablestream-set-up + static already_AddRefed<ReadableStream> CreateNative( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsWrapper& aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + + // https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support + + protected: + // Sets up the ReadableStream with byte reading support. Intended for + // subclasses. + void SetUpByteNative(JSContext* aCx, + UnderlyingSourceAlgorithmsWrapper& aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, ErrorResult& aRv); + + public: + // Creates and sets up a ReadableStream with byte reading support. Use + // SetUpByteNative for this purpose in subclasses. + static already_AddRefed<ReadableStream> CreateByteNative( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSourceAlgorithmsWrapper& aAlgorithms, + mozilla::Maybe<double> aHighWaterMark, ErrorResult& aRv); + + // The following algorithms must only be used on ReadableStream instances + // initialized via the above set up or set up with byte reading support + // algorithms (not, e.g., on web-developer-created instances): + + // https://streams.spec.whatwg.org/#readablestream-close + MOZ_CAN_RUN_SCRIPT void CloseNative(JSContext* aCx, ErrorResult& aRv); + + // https://streams.spec.whatwg.org/#readablestream-error + void ErrorNative(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv); + + // https://streams.spec.whatwg.org/#readablestream-enqueue + MOZ_CAN_RUN_SCRIPT void EnqueueNative(JSContext* aCx, + JS::Handle<JS::Value> aChunk, + ErrorResult& aRv); + + // https://streams.spec.whatwg.org/#readablestream-current-byob-request-view + void GetCurrentBYOBRequestView(JSContext* aCx, + JS::MutableHandle<JSObject*> aView, + ErrorResult& aRv); + + // The following algorithms can be used on arbitrary ReadableStream instances, + // including ones that are created by web developers. They can all fail in + // various operation-specific ways, and these failures should be handled by + // the calling specification. + + // https://streams.spec.whatwg.org/#readablestream-get-a-reader + already_AddRefed<mozilla::dom::ReadableStreamDefaultReader> GetReader( + ErrorResult& aRv); + + // IDL layer functions + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // IDL methods + + // TODO: Use MOZ_CAN_RUN_SCRIPT when IDL constructors can use it (bug 1749042) + MOZ_CAN_RUN_SCRIPT_BOUNDARY static already_AddRefed<ReadableStream> + Constructor(const GlobalObject& aGlobal, + const Optional<JS::Handle<JSObject*>>& aUnderlyingSource, + const QueuingStrategy& aStrategy, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT static already_AddRefed<ReadableStream> From( + const GlobalObject& aGlobal, JS::Handle<JS::Value> asyncIterable, + ErrorResult& aRv); + + bool Locked() const; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Cancel( + JSContext* cx, JS::Handle<JS::Value> aReason, ErrorResult& aRv); + + void GetReader(const ReadableStreamGetReaderOptions& aOptions, + OwningReadableStreamReader& resultReader, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<ReadableStream> PipeThrough( + const ReadableWritablePair& aTransform, const StreamPipeOptions& aOptions, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PipeTo( + WritableStream& aDestination, const StreamPipeOptions& aOptions, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void Tee(JSContext* aCx, + nsTArray<RefPtr<ReadableStream>>& aResult, + ErrorResult& aRv); + + struct IteratorData { + void Traverse(nsCycleCollectionTraversalCallback& cb); + void Unlink(); + + RefPtr<ReadableStreamDefaultReader> mReader; + bool mPreventCancel; + }; + + using Iterator = AsyncIterableIterator<ReadableStream>; + + void InitAsyncIteratorData(IteratorData& aData, Iterator::IteratorType aType, + const ReadableStreamIteratorOptions& aOptions, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> GetNextIterationResult( + Iterator* aIterator, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> IteratorReturn( + JSContext* aCx, Iterator* aIterator, JS::Handle<JS::Value> aValue, + ErrorResult& aRv); + + // Internal Slots: + private: + RefPtr<ReadableStreamController> mController; + bool mDisturbed = false; + RefPtr<ReadableStreamGenericReader> mReader; + ReaderState mState = ReaderState::Readable; + JS::Heap<JS::Value> mStoredError; + + HoldDropJSObjectsCaller mHoldDropCaller; +}; + +namespace streams_abstract { + +bool IsReadableStreamLocked(ReadableStream* aStream); + +double ReadableStreamGetNumReadRequests(ReadableStream* aStream); + +void ReadableStreamError(JSContext* aCx, ReadableStream* aStream, + JS::Handle<JS::Value> aValue, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableStreamClose(JSContext* aCx, + ReadableStream* aStream, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableStreamFulfillReadRequest( + JSContext* aCx, ReadableStream* aStream, JS::Handle<JS::Value> aChunk, + bool done, ErrorResult& aRv); + +void ReadableStreamAddReadRequest(ReadableStream* aStream, + ReadRequest* aReadRequest); +void ReadableStreamAddReadIntoRequest(ReadableStream* aStream, + ReadIntoRequest* aReadIntoRequest); + +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> ReadableStreamCancel( + JSContext* aCx, ReadableStream* aStream, JS::Handle<JS::Value> aError, + ErrorResult& aRv); + +already_AddRefed<ReadableStreamDefaultReader> +AcquireReadableStreamDefaultReader(ReadableStream* aStream, ErrorResult& aRv); + +bool ReadableStreamHasBYOBReader(ReadableStream* aStream); +bool ReadableStreamHasDefaultReader(ReadableStream* aStream); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReadableStream_h diff --git a/dom/streams/ReadableStreamBYOBReader.cpp b/dom/streams/ReadableStreamBYOBReader.cpp new file mode 100644 index 0000000000..f40d98bb93 --- /dev/null +++ b/dom/streams/ReadableStreamBYOBReader.cpp @@ -0,0 +1,359 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ReadableStreamBYOBReader.h" + +#include "ReadIntoRequest.h" +#include "js/ArrayBuffer.h" +#include "js/experimental/TypedData.h" +#include "mozilla/dom/ReadableStreamBYOBReader.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamBYOBReaderBinding.h" +#include "mozilla/dom/ReadableStreamGenericReader.h" +#include "mozilla/dom/RootedDictionary.h" +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" + +// Temporary Includes +#include "mozilla/dom/ReadableByteStreamController.h" +#include "mozilla/dom/ReadableStreamBYOBRequest.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(ReadableStreamBYOBReader, + ReadableStreamGenericReader, + mReadIntoRequests) +NS_IMPL_ADDREF_INHERITED(ReadableStreamBYOBReader, ReadableStreamGenericReader) +NS_IMPL_RELEASE_INHERITED(ReadableStreamBYOBReader, ReadableStreamGenericReader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamBYOBReader) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(ReadableStreamGenericReader) + +ReadableStreamBYOBReader::ReadableStreamBYOBReader(nsISupports* aGlobal) + : ReadableStreamGenericReader(do_QueryInterface(aGlobal)) {} + +JSObject* ReadableStreamBYOBReader::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ReadableStreamBYOBReader_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://streams.spec.whatwg.org/#set-up-readable-stream-byob-reader +void SetUpReadableStreamBYOBReader(ReadableStreamBYOBReader* reader, + ReadableStream& stream, ErrorResult& rv) { + // Step 1. If !IsReadableStreamLocked(stream) is true, throw a TypeError + // exception. + if (IsReadableStreamLocked(&stream)) { + rv.ThrowTypeError("Trying to read locked stream"); + return; + } + + // Step 2. If stream.[[controller]] does not implement + // ReadableByteStreamController, throw a TypeError exception. + if (!stream.Controller()->IsByte()) { + rv.ThrowTypeError("Trying to read with incompatible controller"); + return; + } + + // Step 3. Perform ! ReadableStreamReaderGenericInitialize(reader, stream). + ReadableStreamReaderGenericInitialize(reader, &stream); + + // Step 4. Set reader.[[readIntoRequests]] to a new empty list. + reader->ReadIntoRequests().clear(); +} + +// https://streams.spec.whatwg.org/#byob-reader-constructor +/* static */ already_AddRefed<ReadableStreamBYOBReader> +ReadableStreamBYOBReader::Constructor(const GlobalObject& global, + ReadableStream& stream, ErrorResult& rv) { + nsCOMPtr<nsIGlobalObject> globalObject = + do_QueryInterface(global.GetAsSupports()); + RefPtr<ReadableStreamBYOBReader> reader = + new ReadableStreamBYOBReader(globalObject); + + // Step 1. + SetUpReadableStreamBYOBReader(reader, stream, rv); + if (rv.Failed()) { + return nullptr; + } + + return reader.forget(); +} + +struct Read_ReadIntoRequest final : public ReadIntoRequest { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Read_ReadIntoRequest, + ReadIntoRequest) + + RefPtr<Promise> mPromise; + + explicit Read_ReadIntoRequest(Promise* aPromise) : mPromise(aPromise) {} + + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + MOZ_ASSERT(aChunk.isObject()); + // https://streams.spec.whatwg.org/#byob-reader-read Step 6. + // + // chunk steps, given chunk: + // Resolve promise with «[ "value" → chunk, "done" → false ]». + + // We need to wrap this as the chunk could have come from + // another compartment. + JS::Rooted<JSObject*> chunk(aCx, &aChunk.toObject()); + if (!JS_WrapObject(aCx, &chunk)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + RootedDictionary<ReadableStreamReadResult> result(aCx); + result.mValue = aChunk; + result.mDone.Construct(false); + + mPromise->MaybeResolve(result); + } + + void CloseSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + MOZ_ASSERT(aChunk.isObject() || aChunk.isUndefined()); + // https://streams.spec.whatwg.org/#byob-reader-read Step 6. + // + // close steps, given chunk: + // Resolve promise with «[ "value" → chunk, "done" → true ]». + RootedDictionary<ReadableStreamReadResult> result(aCx); + if (aChunk.isObject()) { + // We need to wrap this as the chunk could have come from + // another compartment. + JS::Rooted<JSObject*> chunk(aCx, &aChunk.toObject()); + if (!JS_WrapObject(aCx, &chunk)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + result.mValue = aChunk; + } + result.mDone.Construct(true); + + mPromise->MaybeResolve(result); + } + + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e, + ErrorResult& aRv) override { + // https://streams.spec.whatwg.org/#byob-reader-read Step 6. + // + // error steps, given e: + // Reject promise with e. + mPromise->MaybeReject(e); + } + + protected: + ~Read_ReadIntoRequest() override = default; +}; + +NS_IMPL_CYCLE_COLLECTION(ReadIntoRequest) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadIntoRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadIntoRequest) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadIntoRequest) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_INHERITED(Read_ReadIntoRequest, ReadIntoRequest, + mPromise) +NS_IMPL_ADDREF_INHERITED(Read_ReadIntoRequest, ReadIntoRequest) +NS_IMPL_RELEASE_INHERITED(Read_ReadIntoRequest, ReadIntoRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Read_ReadIntoRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadIntoRequest) + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-stream-byob-reader-read +void ReadableStreamBYOBReaderRead(JSContext* aCx, + ReadableStreamBYOBReader* aReader, + JS::Handle<JSObject*> aView, + ReadIntoRequest* aReadIntoRequest, + ErrorResult& aRv) { + // Step 1.Let stream be reader.[[stream]]. + ReadableStream* stream = aReader->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Set stream.[[disturbed]] to true. + stream->SetDisturbed(true); + + // Step 4. If stream.[[state]] is "errored", perform readIntoRequest’s error + // steps given stream.[[storedError]]. + if (stream->State() == ReadableStream::ReaderState::Errored) { + JS::Rooted<JS::Value> error(aCx, stream->StoredError()); + + aReadIntoRequest->ErrorSteps(aCx, error, aRv); + return; + } + + // Step 5. Otherwise, perform + // !ReadableByteStreamControllerPullInto(stream.[[controller]], view, + // readIntoRequest). + MOZ_ASSERT(stream->Controller()->IsByte()); + RefPtr<ReadableByteStreamController> controller( + stream->Controller()->AsByte()); + ReadableByteStreamControllerPullInto(aCx, controller, aView, aReadIntoRequest, + aRv); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#byob-reader-read +already_AddRefed<Promise> ReadableStreamBYOBReader::Read( + const ArrayBufferView& aArray, ErrorResult& aRv) { + AutoJSAPI jsapi; + if (!jsapi.Init(GetParentObject())) { + aRv.ThrowUnknownError("Internal error"); + return nullptr; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted<JSObject*> view(cx, aArray.Obj()); + + // Step 1. If view.[[ByteLength]] is 0, return a promise rejected with a + // TypeError exception. + if (JS_GetArrayBufferViewByteLength(view) == 0) { + // Binding code should convert this thrown value into a rejected promise. + aRv.ThrowTypeError("Zero Length View"); + return nullptr; + } + + // Step 2. If view.[[ViewedArrayBuffer]].[[ArrayBufferByteLength]] is 0, + // return a promise rejected with a TypeError exception. + bool isSharedMemory; + JS::Rooted<JSObject*> viewedArrayBuffer( + cx, JS_GetArrayBufferViewBuffer(cx, view, &isSharedMemory)); + if (!viewedArrayBuffer) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + + if (JS::GetArrayBufferByteLength(viewedArrayBuffer) == 0) { + aRv.ThrowTypeError("zero length viewed buffer"); + return nullptr; + } + + // Step 3. If ! IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true, return a + // promise rejected with a TypeError exception. + if (JS::IsDetachedArrayBufferObject(viewedArrayBuffer)) { + aRv.ThrowTypeError("Detached Buffer"); + return nullptr; + } + + // Step 4. If this.[[stream]] is undefined, return a promise rejected with a + // TypeError exception. + if (!GetStream()) { + aRv.ThrowTypeError("Reader has undefined stream"); + return nullptr; + } + + // Step 5. + RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject()); + + // Step 6. Let readIntoRequest be a new read-into request with the following + // items: + RefPtr<ReadIntoRequest> readIntoRequest = new Read_ReadIntoRequest(promise); + + // Step 7. Perform ! ReadableStreamBYOBReaderRead(this, view, + // readIntoRequest). + ReadableStreamBYOBReaderRead(cx, this, view, readIntoRequest, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 8. Return promise. + return promise.forget(); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreambyobreadererrorreadintorequests +void ReadableStreamBYOBReaderErrorReadIntoRequests( + JSContext* aCx, ReadableStreamBYOBReader* aReader, + JS::Handle<JS::Value> aError, ErrorResult& aRv) { + // Step 1. Let readIntoRequests be reader.[[readIntoRequests]]. + LinkedList<RefPtr<ReadIntoRequest>> readIntoRequests = + std::move(aReader->ReadIntoRequests()); + + // Step 2. Set reader.[[readIntoRequests]] to a new empty list. + // Note: The std::move already cleared this anyway. + aReader->ReadIntoRequests().clear(); + + // Step 3. For each readIntoRequest of readIntoRequests, + while (RefPtr<ReadIntoRequest> readIntoRequest = + readIntoRequests.popFirst()) { + // Step 3.1. Perform readIntoRequest’s error steps, given e. + readIntoRequest->ErrorSteps(aCx, aError, aRv); + if (aRv.Failed()) { + return; + } + } +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreambyobreaderrelease +void ReadableStreamBYOBReaderRelease(JSContext* aCx, + ReadableStreamBYOBReader* aReader, + ErrorResult& aRv) { + // Step 1. Perform ! ReadableStreamReaderGenericRelease(reader). + ReadableStreamReaderGenericRelease(aReader, aRv); + if (aRv.Failed()) { + return; + } + + // Step 2. Let e be a new TypeError exception. + ErrorResult rv; + rv.ThrowTypeError("Releasing lock"); + JS::Rooted<JS::Value> error(aCx); + MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), &error)); + + // Step 3. Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e). + ReadableStreamBYOBReaderErrorReadIntoRequests(aCx, aReader, error, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#byob-reader-release-lock +void ReadableStreamBYOBReader::ReleaseLock(ErrorResult& aRv) { + // Step 1. If this.[[stream]] is undefined, return. + if (!mStream) { + return; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + return aRv.ThrowUnknownError("Internal error"); + } + JSContext* cx = jsapi.cx(); + + // Step 2. Perform ! ReadableStreamBYOBReaderRelease(this). + RefPtr<ReadableStreamBYOBReader> thisRefPtr = this; + ReadableStreamBYOBReaderRelease(cx, thisRefPtr, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#acquire-readable-stream-byob-reader +already_AddRefed<ReadableStreamBYOBReader> AcquireReadableStreamBYOBReader( + ReadableStream* aStream, ErrorResult& aRv) { + // Step 1. Let reader be a new ReadableStreamBYOBReader. + RefPtr<ReadableStreamBYOBReader> reader = + new ReadableStreamBYOBReader(aStream->GetParentObject()); + + // Step 2. Perform ? SetUpReadableStreamBYOBReader(reader, stream). + SetUpReadableStreamBYOBReader(reader, *aStream, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 3. Return reader. + return reader.forget(); +} +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStreamBYOBReader.h b/dom/streams/ReadableStreamBYOBReader.h new file mode 100644 index 0000000000..896f1acbf9 --- /dev/null +++ b/dom/streams/ReadableStreamBYOBReader.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamBYOBReader_h +#define mozilla_dom_ReadableStreamBYOBReader_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ReadableStreamGenericReader.h" +#include "mozilla/dom/TypedArray.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "mozilla/LinkedList.h" + +namespace mozilla::dom { + +class Promise; +struct ReadIntoRequest; +class ReadableStream; + +} // namespace mozilla::dom + +namespace mozilla::dom { + +class ReadableStreamBYOBReader final : public ReadableStreamGenericReader, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_INHERITED( + ReadableStreamBYOBReader, ReadableStreamGenericReader) + + public: + explicit ReadableStreamBYOBReader(nsISupports* aGlobal); + + bool IsDefault() override { return false; }; + bool IsBYOB() override { return true; } + ReadableStreamDefaultReader* AsDefault() override { + MOZ_CRASH("Should have verified IsDefault first"); + return nullptr; + } + ReadableStreamBYOBReader* AsBYOB() override { return this; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<ReadableStreamBYOBReader> Constructor( + const GlobalObject& global, ReadableStream& stream, ErrorResult& rv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Read( + const ArrayBufferView& aArray, ErrorResult& rv); + + void ReleaseLock(ErrorResult& rv); + + LinkedList<RefPtr<ReadIntoRequest>>& ReadIntoRequests() { + return mReadIntoRequests; + } + + private: + ~ReadableStreamBYOBReader() override = default; + + LinkedList<RefPtr<ReadIntoRequest>> mReadIntoRequests; +}; + +namespace streams_abstract { + +already_AddRefed<ReadableStreamBYOBReader> AcquireReadableStreamBYOBReader( + ReadableStream* aStream, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableStreamBYOBReaderRead( + JSContext* aCx, ReadableStreamBYOBReader* aReader, + JS::Handle<JSObject*> aView, ReadIntoRequest* aReadIntoRequest, + ErrorResult& aRv); + +void ReadableStreamBYOBReaderErrorReadIntoRequests( + JSContext* aCx, ReadableStreamBYOBReader* aReader, + JS::Handle<JS::Value> aError, ErrorResult& aRv); + +void ReadableStreamBYOBReaderRelease(JSContext* aCx, + ReadableStreamBYOBReader* aReader, + ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReadableStreamBYOBReader_h diff --git a/dom/streams/ReadableStreamBYOBRequest.cpp b/dom/streams/ReadableStreamBYOBRequest.cpp new file mode 100644 index 0000000000..655b914830 --- /dev/null +++ b/dom/streams/ReadableStreamBYOBRequest.cpp @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ReadableStreamBYOBRequest.h" + +#include "mozilla/dom/ByteStreamHelpers.h" +#include "js/ArrayBuffer.h" +#include "js/TypeDecls.h" +#include "mozilla/dom/ReadableByteStreamController.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamBYOBRequestBinding.h" +#include "js/experimental/TypedData.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "nsCOMPtr.h" +#include "nsIGlobalObject.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +ReadableStreamBYOBRequest::ReadableStreamBYOBRequest(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) { + mozilla::HoldJSObjects(this); +} + +ReadableStreamBYOBRequest::~ReadableStreamBYOBRequest() { + mozilla::DropJSObjects(this); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS(ReadableStreamBYOBRequest, + (mGlobal, mController), + (mView)) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadableStreamBYOBRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadableStreamBYOBRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamBYOBRequest) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* ReadableStreamBYOBRequest::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ReadableStreamBYOBRequest_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://streams.spec.whatwg.org/#rs-byob-request-view +void ReadableStreamBYOBRequest::GetView( + JSContext* cx, JS::MutableHandle<JSObject*> aRetVal) const { + // Step 1. + aRetVal.set(mView); +} + +// https://streams.spec.whatwg.org/#rs-byob-request-respond +void ReadableStreamBYOBRequest::Respond(JSContext* aCx, uint64_t bytesWritten, + ErrorResult& aRv) { + // Step 1. + if (!mController) { + aRv.ThrowTypeError("Undefined Controller"); + return; + } + + // Step 2. + bool isSharedMemory; + JS::Rooted<JSObject*> view(aCx, mView); + JS::Rooted<JSObject*> arrayBuffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, view, &isSharedMemory)); + if (!arrayBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + if (JS::IsDetachedArrayBufferObject(arrayBuffer)) { + aRv.ThrowTypeError("View of Detached buffer"); + return; + } + + // Step 3. + MOZ_ASSERT(JS_GetArrayBufferViewByteLength(view) > 0); + + // Step 4. + MOZ_ASSERT(JS::GetArrayBufferByteLength(arrayBuffer) > 0); + + // Step 5. + RefPtr<ReadableByteStreamController> controller(mController); + ReadableByteStreamControllerRespond(aCx, controller, bytesWritten, aRv); +} + +// https://streams.spec.whatwg.org/#rs-byob-request-respond-with-new-view +void ReadableStreamBYOBRequest::RespondWithNewView(JSContext* aCx, + const ArrayBufferView& view, + ErrorResult& aRv) { + // Step 1. + if (!mController) { + aRv.ThrowTypeError("Undefined Controller"); + return; + } + + // Step 2. + bool isSharedMemory; + JS::Rooted<JSObject*> rootedViewObj(aCx, view.Obj()); + JS::Rooted<JSObject*> viewedArrayBuffer( + aCx, JS_GetArrayBufferViewBuffer(aCx, rootedViewObj, &isSharedMemory)); + if (!viewedArrayBuffer) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + if (JS::IsDetachedArrayBufferObject(viewedArrayBuffer)) { + aRv.ThrowTypeError("View of Detached Array Buffer"); + return; + } + + // Step 3. + RefPtr<ReadableByteStreamController> controller(mController); + ReadableByteStreamControllerRespondWithNewView(aCx, controller, rootedViewObj, + aRv); +} + +void ReadableStreamBYOBRequest::SetController( + ReadableByteStreamController* aController) { + mController = aController; +} + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStreamBYOBRequest.h b/dom/streams/ReadableStreamBYOBRequest.h new file mode 100644 index 0000000000..fb8ad62979 --- /dev/null +++ b/dom/streams/ReadableStreamBYOBRequest.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamBYOBRequest_h +#define mozilla_dom_ReadableStreamBYOBRequest_h + +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/TypedArray.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +class ReadableByteStreamController; + +class ReadableStreamBYOBRequest final : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ReadableStreamBYOBRequest) + + public: + explicit ReadableStreamBYOBRequest(nsIGlobalObject* aGlobal); + + protected: + ~ReadableStreamBYOBRequest(); + + public: + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetView(JSContext* cx, JS::MutableHandle<JSObject*> aRetVal) const; + + MOZ_CAN_RUN_SCRIPT void Respond(JSContext* aCx, uint64_t bytesWritten, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void RespondWithNewView(JSContext* aCx, + const ArrayBufferView& view, + ErrorResult& aRv); + + ReadableByteStreamController* Controller() { return mController; } + void SetController(ReadableByteStreamController* aController); + + JSObject* View() { return mView; } + void SetView(JS::Handle<JSObject*> aView) { mView = aView; } + + private: + // Internal Slots + nsCOMPtr<nsIGlobalObject> mGlobal; + RefPtr<ReadableByteStreamController> mController; + JS::Heap<JSObject*> mView; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadableStreamController.h b/dom/streams/ReadableStreamController.h new file mode 100644 index 0000000000..766c933017 --- /dev/null +++ b/dom/streams/ReadableStreamController.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamController_h +#define mozilla_dom_ReadableStreamController_h + +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" +#include "UnderlyingSourceCallbackHelpers.h" + +namespace mozilla::dom { +struct ReadRequest; +class ReadableStream; +class ReadableStreamDefaultController; +class ReadableByteStreamController; + +class ReadableStreamController : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ReadableStreamController) + + ReadableStreamController(nsIGlobalObject* aGlobal); + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + virtual bool IsDefault() = 0; + virtual bool IsByte() = 0; + virtual ReadableStreamDefaultController* AsDefault() = 0; + virtual ReadableByteStreamController* AsByte() = 0; + + MOZ_CAN_RUN_SCRIPT + virtual already_AddRefed<Promise> CancelSteps(JSContext* aCx, + JS::Handle<JS::Value> aReason, + ErrorResult& aRv) = 0; + MOZ_CAN_RUN_SCRIPT + virtual void PullSteps(JSContext* aCx, ReadRequest* aReadRequest, + ErrorResult& aRv) = 0; + + // No JS implementable UnderlyingSource callback exists for this. + virtual void ReleaseSteps() = 0; + + UnderlyingSourceAlgorithmsBase* GetAlgorithms() const { return mAlgorithms; } + void SetAlgorithms(UnderlyingSourceAlgorithmsBase& aAlgorithms) { + mAlgorithms = &aAlgorithms; + } + void ClearAlgorithms() { + MOZ_ASSERT(mAlgorithms); + mAlgorithms->ReleaseObjects(); + mAlgorithms = nullptr; + } + + ReadableStream* Stream() const { return mStream; } + void SetStream(ReadableStream* aStream); + + protected: + nsCOMPtr<nsIGlobalObject> mGlobal; + + // The algorithms for the underlying source + RefPtr<UnderlyingSourceAlgorithmsBase> mAlgorithms; + + RefPtr<ReadableStream> mStream; + + virtual ~ReadableStreamController() = default; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadableStreamDefaultController.cpp b/dom/streams/ReadableStreamDefaultController.cpp new file mode 100644 index 0000000000..7a5ae3a80f --- /dev/null +++ b/dom/streams/ReadableStreamDefaultController.cpp @@ -0,0 +1,653 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ReadableStreamDefaultController.h" + +#include "js/Exception.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/ReadableStreamDefaultControllerBinding.h" +#include "mozilla/dom/ReadableStreamDefaultReaderBinding.h" +#include "mozilla/dom/UnderlyingSourceBinding.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION(ReadableStreamController, mGlobal, mAlgorithms, + mStream) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadableStreamController) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadableStreamController) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamController) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ReadableStreamController::ReadableStreamController(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) {} + +void ReadableStreamController::SetStream(ReadableStream* aStream) { + mStream = aStream; +} + +// Note: Using the individual macros vs NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE +// because I need to specify a manual implementation of +// NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN. +NS_IMPL_CYCLE_COLLECTION_CLASS(ReadableStreamDefaultController) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ReadableStreamDefaultController, + ReadableStreamController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStrategySizeAlgorithm) + tmp->mQueue.clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + ReadableStreamDefaultController, ReadableStreamController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStrategySizeAlgorithm) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ReadableStreamDefaultController, + ReadableStreamController) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + // Trace the associated queue. + for (const auto& queueEntry : tmp->mQueue) { + aCallbacks.Trace(&queueEntry->mValue, "mQueue.mValue", aClosure); + } +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(ReadableStreamDefaultController, + ReadableStreamController) +NS_IMPL_RELEASE_INHERITED(ReadableStreamDefaultController, + ReadableStreamController) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamDefaultController) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(ReadableStreamController) + +ReadableStreamDefaultController::ReadableStreamDefaultController( + nsIGlobalObject* aGlobal) + : ReadableStreamController(aGlobal) { + mozilla::HoldJSObjects(this); +} + +ReadableStreamDefaultController::~ReadableStreamDefaultController() { + // MG:XXX: LinkedLists are required to be empty at destruction, but it seems + // it is possible to have a controller be destructed while still + // having entries in its queue. + // + // This needs to be verified as not indicating some other issue. + mozilla::DropJSObjects(this); + mQueue.clear(); +} + +JSObject* ReadableStreamDefaultController::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ReadableStreamDefaultController_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-can-close-or-enqueue +static bool ReadableStreamDefaultControllerCanCloseOrEnqueue( + ReadableStreamDefaultController* aController) { + // Step 1. Let state be controller.[[stream]].[[state]]. + ReadableStream::ReaderState state = aController->Stream()->State(); + + // Step 2. If controller.[[closeRequested]] is false and state is "readable", + // return true. + // Step 3. Return false. + return !aController->CloseRequested() && + state == ReadableStream::ReaderState::Readable; +} + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-can-close-or-enqueue +// This is a variant of ReadableStreamDefaultControllerCanCloseOrEnqueue +// that also throws when the function would return false to improve error +// messages. +bool ReadableStreamDefaultControllerCanCloseOrEnqueueAndThrow( + ReadableStreamDefaultController* aController, + CloseOrEnqueue aCloseOrEnqueue, ErrorResult& aRv) { + // Step 1. Let state be controller.[[stream]].[[state]]. + ReadableStream::ReaderState state = aController->Stream()->State(); + + nsCString prefix; + if (aCloseOrEnqueue == CloseOrEnqueue::Close) { + prefix = "Cannot close a stream that "_ns; + } else { + prefix = "Cannot enqueue into a stream that "_ns; + } + + switch (state) { + case ReadableStream::ReaderState::Readable: + // Step 2. If controller.[[closeRequested]] is false and + // state is "readable", return true. + // Note: We don't error/check for [[closeRequest]] first, because + // [[closedRequest]] is still true even after the state is "closed". + // This doesn't cause any spec observable difference. + if (!aController->CloseRequested()) { + return true; + } + + // Step 3. Return false. + aRv.ThrowTypeError(prefix + "has already been requested to close."_ns); + return false; + + case ReadableStream::ReaderState::Closed: + aRv.ThrowTypeError(prefix + "is already closed."_ns); + return false; + + case ReadableStream::ReaderState::Errored: + aRv.ThrowTypeError(prefix + "has errored."_ns); + return false; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown ReaderState"); + return false; + } +} + +Nullable<double> ReadableStreamDefaultControllerGetDesiredSize( + ReadableStreamDefaultController* aController) { + ReadableStream::ReaderState state = aController->Stream()->State(); + if (state == ReadableStream::ReaderState::Errored) { + return nullptr; + } + + if (state == ReadableStream::ReaderState::Closed) { + return 0.0; + } + + return aController->StrategyHWM() - aController->QueueTotalSize(); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-default-controller-desired-size +Nullable<double> ReadableStreamDefaultController::GetDesiredSize() { + // Step 1. + return ReadableStreamDefaultControllerGetDesiredSize(this); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-clear-algorithms +// +// Note: nullptr is used to indicate we run the default algorithm at the +// moment, +// so the below doesn't quite match the spec, but serves the correct +// purpose for disconnecting the algorithms from the object graph to allow +// collection. +// +// As far as I know, this isn't currently visible, but we need to keep +// this in mind. This is a weakness of this current implementation, and +// I'd prefer to have a better answer here eventually. +void ReadableStreamDefaultControllerClearAlgorithms( + ReadableStreamDefaultController* aController) { + // Step 1. + // Step 2. + aController->ClearAlgorithms(); + + // Step 3. + aController->setStrategySizeAlgorithm(nullptr); +} + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-close +void ReadableStreamDefaultControllerClose( + JSContext* aCx, ReadableStreamDefaultController* aController, + ErrorResult& aRv) { + // Step 1. + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(aController)) { + return; + } + + // Step 2. + RefPtr<ReadableStream> stream = aController->Stream(); + + // Step 3. + aController->SetCloseRequested(true); + + // Step 4. + if (aController->Queue().isEmpty()) { + // Step 4.1 + ReadableStreamDefaultControllerClearAlgorithms(aController); + + // Step 4.2 + ReadableStreamClose(aCx, stream, aRv); + } +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-default-controller-close +void ReadableStreamDefaultController::Close(JSContext* aCx, ErrorResult& aRv) { + // Step 1. + if (!ReadableStreamDefaultControllerCanCloseOrEnqueueAndThrow( + this, CloseOrEnqueue::Close, aRv)) { + return; + } + + // Step 2. + ReadableStreamDefaultControllerClose(aCx, this, aRv); +} + +namespace streams_abstract { + +MOZ_CAN_RUN_SCRIPT static void ReadableStreamDefaultControllerCallPullIfNeeded( + JSContext* aCx, ReadableStreamDefaultController* aController, + ErrorResult& aRv); + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-enqueue +void ReadableStreamDefaultControllerEnqueue( + JSContext* aCx, ReadableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1. + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(aController)) { + return; + } + + // Step 2. + RefPtr<ReadableStream> stream = aController->Stream(); + + // Step 3. + if (IsReadableStreamLocked(stream) && + ReadableStreamGetNumReadRequests(stream) > 0) { + ReadableStreamFulfillReadRequest(aCx, stream, aChunk, false, aRv); + } else { + // Step 4.1 + Optional<JS::Handle<JS::Value>> optionalChunk(aCx, aChunk); + + // Step 4.3 (Re-ordered); + RefPtr<QueuingStrategySize> sizeAlgorithm( + aController->StrategySizeAlgorithm()); + + // If !sizeAlgorithm, we return 1, which is inlined from + // https://streams.spec.whatwg.org/#make-size-algorithm-from-size-function + double chunkSize = + sizeAlgorithm + ? sizeAlgorithm->Call( + optionalChunk, aRv, + "ReadableStreamDefaultController.[[strategySizeAlgorithm]]", + CallbackObject::eRethrowExceptions) + : 1.0; + + // If this is an uncatchable exception we can't continue. + if (aRv.IsUncatchableException()) { + return; + } + + // Step 4.2: + if (aRv.MaybeSetPendingException( + aCx, "ReadableStreamDefaultController.enqueue")) { + JS::Rooted<JS::Value> errorValue(aCx); + + JS_GetPendingException(aCx, &errorValue); + + // Step 4.2.1 + + ReadableStreamDefaultControllerError(aCx, aController, errorValue, aRv); + if (aRv.Failed()) { + return; + } + + // Step 4.2.2 Caller must treat aRv as if it were a completion + // value + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, errorValue); + return; + } + + // Step 4.4 + EnqueueValueWithSize(aController, aChunk, chunkSize, aRv); + + // Step 4.5 + // Note we convert the pending exception to a JS value here, and then + // re-throw it because we save this exception and re-expose it elsewhere + // and there are tests to ensure the identity of these errors are the same. + if (aRv.MaybeSetPendingException( + aCx, "ReadableStreamDefaultController.enqueue")) { + JS::Rooted<JS::Value> errorValue(aCx); + + if (!JS_GetPendingException(aCx, &errorValue)) { + // Uncatchable exception; we should mark aRv and return. + aRv.StealExceptionFromJSContext(aCx); + return; + } + JS_ClearPendingException(aCx); + + // Step 4.5.1 + ReadableStreamDefaultControllerError(aCx, aController, errorValue, aRv); + if (aRv.Failed()) { + return; + } + + // Step 4.5.2 Caller must treat aRv as if it were a completion + // value + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, errorValue); + return; + } + } + + // Step 5. + ReadableStreamDefaultControllerCallPullIfNeeded(aCx, aController, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-default-controller-close +void ReadableStreamDefaultController::Enqueue(JSContext* aCx, + JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) { + // Step 1. + if (!ReadableStreamDefaultControllerCanCloseOrEnqueueAndThrow( + this, CloseOrEnqueue::Enqueue, aRv)) { + return; + } + + // Step 2. + ReadableStreamDefaultControllerEnqueue(aCx, this, aChunk, aRv); +} + +void ReadableStreamDefaultController::Error(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + ReadableStreamDefaultControllerError(aCx, this, aError, aRv); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-should-call-pull +bool ReadableStreamDefaultControllerShouldCallPull( + ReadableStreamDefaultController* aController) { + // Step 1. + ReadableStream* stream = aController->Stream(); + + // Step 2. + if (!ReadableStreamDefaultControllerCanCloseOrEnqueue(aController)) { + return false; + } + + // Step 3. + if (!aController->Started()) { + return false; + } + + // Step 4. + if (IsReadableStreamLocked(stream) && + ReadableStreamGetNumReadRequests(stream) > 0) { + return true; + } + + // Step 5. + Nullable<double> desiredSize = + ReadableStreamDefaultControllerGetDesiredSize(aController); + + // Step 6. + MOZ_ASSERT(!desiredSize.IsNull()); + + // Step 7 + 8 + return desiredSize.Value() > 0; +} + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-error +void ReadableStreamDefaultControllerError( + JSContext* aCx, ReadableStreamDefaultController* aController, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + // Step 1. + ReadableStream* stream = aController->Stream(); + + // Step 2. + if (stream->State() != ReadableStream::ReaderState::Readable) { + return; + } + + // Step 3. + ResetQueue(aController); + + // Step 4. + ReadableStreamDefaultControllerClearAlgorithms(aController); + + // Step 5. + ReadableStreamError(aCx, stream, aValue, aRv); +} + +// https://streams.spec.whatwg.org/#readable-stream-default-controller-call-pull-if-needed +static void ReadableStreamDefaultControllerCallPullIfNeeded( + JSContext* aCx, ReadableStreamDefaultController* aController, + ErrorResult& aRv) { + // Step 1. + bool shouldPull = ReadableStreamDefaultControllerShouldCallPull(aController); + + // Step 2. + if (!shouldPull) { + return; + } + + // Step 3. + if (aController->Pulling()) { + // Step 3.1 + aController->SetPullAgain(true); + // Step 3.2 + return; + } + + // Step 4. + MOZ_ASSERT(!aController->PullAgain()); + + // Step 5. + aController->SetPulling(true); + + // Step 6. + RefPtr<UnderlyingSourceAlgorithmsBase> algorithms = + aController->GetAlgorithms(); + RefPtr<Promise> pullPromise = + algorithms->PullCallback(aCx, *aController, aRv); + if (aRv.Failed()) { + return; + } + + // Step 7 + 8: + pullPromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableStreamDefaultController* mController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + // Step 7.1 + mController->SetPulling(false); + // Step 7.2 + if (mController->PullAgain()) { + // Step 7.2.1 + mController->SetPullAgain(false); + + // Step 7.2.2 + ErrorResult rv; + ReadableStreamDefaultControllerCallPullIfNeeded( + aCx, MOZ_KnownLive(mController), aRv); + } + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableStreamDefaultController* mController) { + // Step 8.1 + ReadableStreamDefaultControllerError(aCx, mController, aValue, aRv); + }, + RefPtr(aController)); +} + +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller +void SetUpReadableStreamDefaultController( + JSContext* aCx, ReadableStream* aStream, + ReadableStreamDefaultController* aController, + UnderlyingSourceAlgorithmsBase* aAlgorithms, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv) { + // Step 1. + MOZ_ASSERT(!aStream->Controller()); + + // Step 2. + aController->SetStream(aStream); + + // Step 3. + ResetQueue(aController); + + // Step 4. + aController->SetStarted(false); + aController->SetCloseRequested(false); + aController->SetPullAgain(false); + aController->SetPulling(false); + + // Step 5. + aController->setStrategySizeAlgorithm(aSizeAlgorithm); + aController->SetStrategyHWM(aHighWaterMark); + + // Step 6. + // Step 7. + aController->SetAlgorithms(*aAlgorithms); + + // Step 8. + aStream->SetController(*aController); + + // Step 9. Default algorithm returns undefined. See Step 2 of + // https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller + JS::Rooted<JS::Value> startResult(aCx, JS::UndefinedValue()); + RefPtr<ReadableStreamDefaultController> controller = aController; + aAlgorithms->StartCallback(aCx, *controller, &startResult, aRv); + if (aRv.Failed()) { + return; + } + + // Step 10. + RefPtr<Promise> startPromise = + Promise::CreateInfallible(aStream->GetParentObject()); + startPromise->MaybeResolve(startResult); + + // Step 11 & 12: + startPromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableStreamDefaultController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(aController); + + // Step 11.1 + aController->SetStarted(true); + + // Step 11.2 + aController->SetPulling(false); + + // Step 11.3 + aController->SetPullAgain(false); + + // Step 11.4: + ReadableStreamDefaultControllerCallPullIfNeeded( + aCx, MOZ_KnownLive(aController), aRv); + }, + + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + ReadableStreamDefaultController* aController) { + // Step 12.1 + ReadableStreamDefaultControllerError(aCx, aController, aValue, aRv); + }, + RefPtr(aController)); +} + +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller-from-underlying-source +void SetupReadableStreamDefaultControllerFromUnderlyingSource( + JSContext* aCx, ReadableStream* aStream, + JS::Handle<JSObject*> aUnderlyingSource, + UnderlyingSource& aUnderlyingSourceDict, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv) { + // Step 1. + RefPtr<ReadableStreamDefaultController> controller = + new ReadableStreamDefaultController(aStream->GetParentObject()); + + // Step 2 - 7 + RefPtr<UnderlyingSourceAlgorithms> algorithms = + new UnderlyingSourceAlgorithms(aStream->GetParentObject(), + aUnderlyingSource, aUnderlyingSourceDict); + + // Step 8: + SetUpReadableStreamDefaultController(aCx, aStream, controller, algorithms, + aHighWaterMark, aSizeAlgorithm, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#rs-default-controller-private-cancel +already_AddRefed<Promise> ReadableStreamDefaultController::CancelSteps( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1. + ResetQueue(this); + + // Step 2. + Optional<JS::Handle<JS::Value>> errorOption(aCx, aReason); + RefPtr<UnderlyingSourceAlgorithmsBase> algorithms = mAlgorithms; + RefPtr<Promise> result = algorithms->CancelCallback(aCx, errorOption, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 3. + ReadableStreamDefaultControllerClearAlgorithms(this); + + // Step 4. + return result.forget(); +} + +// https://streams.spec.whatwg.org/#rs-default-controller-private-pull +void ReadableStreamDefaultController::PullSteps(JSContext* aCx, + ReadRequest* aReadRequest, + ErrorResult& aRv) { + // Step 1. + RefPtr<ReadableStream> stream = mStream; + + // Step 2. + if (!mQueue.isEmpty()) { + // Step 2.1 + JS::Rooted<JS::Value> chunk(aCx); + DequeueValue(this, &chunk); + + // Step 2.2 + if (CloseRequested() && mQueue.isEmpty()) { + // Step 2.2.1 + ReadableStreamDefaultControllerClearAlgorithms(this); + // Step 2.2.2 + ReadableStreamClose(aCx, stream, aRv); + if (aRv.Failed()) { + return; + } + } else { + // Step 2.3 + ReadableStreamDefaultControllerCallPullIfNeeded(aCx, this, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 2.4 + aReadRequest->ChunkSteps(aCx, chunk, aRv); + } else { + // Step 3. + // Step 3.1 + ReadableStreamAddReadRequest(stream, aReadRequest); + // Step 3.2 + ReadableStreamDefaultControllerCallPullIfNeeded(aCx, this, aRv); + } +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaultcontroller-releasesteps +void ReadableStreamDefaultController::ReleaseSteps() { + // Step 1. Return. +} + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStreamDefaultController.h b/dom/streams/ReadableStreamDefaultController.h new file mode 100644 index 0000000000..f94a692f15 --- /dev/null +++ b/dom/streams/ReadableStreamDefaultController.h @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamDefaultController_h +#define mozilla_dom_ReadableStreamDefaultController_h + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/QueueWithSizes.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/ReadRequest.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/Nullable.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class ReadableStream; +class ReadableStreamDefaultReader; +struct UnderlyingSource; +class ReadableStreamGenericReader; + +class ReadableStreamDefaultController final : public ReadableStreamController, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + ReadableStreamDefaultController, ReadableStreamController) + + public: + explicit ReadableStreamDefaultController(nsIGlobalObject* aGlobal); + + protected: + ~ReadableStreamDefaultController() override; + + public: + bool IsDefault() override { return true; } + bool IsByte() override { return false; } + ReadableStreamDefaultController* AsDefault() override { return this; } + ReadableByteStreamController* AsByte() override { return nullptr; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + Nullable<double> GetDesiredSize(); + + MOZ_CAN_RUN_SCRIPT void Close(JSContext* aCx, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void Enqueue(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv); + + void Error(JSContext* aCx, JS::Handle<JS::Value> aError, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelSteps( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) override; + MOZ_CAN_RUN_SCRIPT void PullSteps(JSContext* aCx, ReadRequest* aReadRequest, + ErrorResult& aRv) override; + + void ReleaseSteps() override; + + // Internal Slot Accessors + bool CloseRequested() const { return mCloseRequested; } + void SetCloseRequested(bool aCloseRequested) { + mCloseRequested = aCloseRequested; + } + + bool PullAgain() const { return mPullAgain; } + void SetPullAgain(bool aPullAgain) { mPullAgain = aPullAgain; } + + bool Pulling() const { return mPulling; } + void SetPulling(bool aPulling) { mPulling = aPulling; } + + QueueWithSizes& Queue() { return mQueue; } + + double QueueTotalSize() const { return mQueueTotalSize; } + void SetQueueTotalSize(double aQueueTotalSize) { + mQueueTotalSize = aQueueTotalSize; + } + + bool Started() const { return mStarted; } + void SetStarted(bool aStarted) { mStarted = aStarted; } + + double StrategyHWM() const { return mStrategyHWM; } + void SetStrategyHWM(double aStrategyHWM) { mStrategyHWM = aStrategyHWM; } + + QueuingStrategySize* StrategySizeAlgorithm() const { + return mStrategySizeAlgorithm; + } + void setStrategySizeAlgorithm(QueuingStrategySize* aStrategySizeAlgorithm) { + mStrategySizeAlgorithm = aStrategySizeAlgorithm; + } + + private: + // Internal Slots: + bool mCloseRequested = false; + bool mPullAgain = false; + bool mPulling = false; + QueueWithSizes mQueue = {}; + double mQueueTotalSize = 0.0; + bool mStarted = false; + double mStrategyHWM = false; + RefPtr<QueuingStrategySize> mStrategySizeAlgorithm; +}; + +namespace streams_abstract { + +MOZ_CAN_RUN_SCRIPT void SetUpReadableStreamDefaultController( + JSContext* aCx, ReadableStream* aStream, + ReadableStreamDefaultController* aController, + UnderlyingSourceAlgorithmsBase* aAlgorithms, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void +SetupReadableStreamDefaultControllerFromUnderlyingSource( + JSContext* aCx, ReadableStream* aStream, + JS::Handle<JSObject*> aUnderlyingSource, + UnderlyingSource& aUnderlyingSourceDict, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableStreamDefaultControllerEnqueue( + JSContext* aCx, ReadableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableStreamDefaultControllerClose( + JSContext* aCx, ReadableStreamDefaultController* aController, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void ReadableStreamDefaultReaderRead( + JSContext* aCx, ReadableStreamGenericReader* reader, ReadRequest* aRequest, + ErrorResult& aRv); + +void ReadableStreamDefaultControllerError( + JSContext* aCx, ReadableStreamDefaultController* aController, + JS::Handle<JS::Value> aValue, ErrorResult& aRv); + +Nullable<double> ReadableStreamDefaultControllerGetDesiredSize( + ReadableStreamDefaultController* aController); + +enum class CloseOrEnqueue { Close, Enqueue }; + +bool ReadableStreamDefaultControllerCanCloseOrEnqueueAndThrow( + ReadableStreamDefaultController* aController, + CloseOrEnqueue aCloseOrEnqueue, ErrorResult& aRv); + +bool ReadableStreamDefaultControllerShouldCallPull( + ReadableStreamDefaultController* aController); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReadableStreamDefaultController_h diff --git a/dom/streams/ReadableStreamDefaultReader.cpp b/dom/streams/ReadableStreamDefaultReader.cpp new file mode 100644 index 0000000000..ebc86d4778 --- /dev/null +++ b/dom/streams/ReadableStreamDefaultReader.cpp @@ -0,0 +1,439 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ReadableStreamDefaultReader.h" + +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/RootedDictionary.h" +#include "js/PropertyAndElement.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "jsapi.h" +#include "mozilla/dom/ReadableStreamDefaultReaderBinding.h" +#include "mozilla/dom/UnderlyingSourceBinding.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION(ReadableStreamGenericReader, mClosedPromise, mStream, + mGlobal) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadableStreamGenericReader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadableStreamGenericReader) +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ReadableStreamGenericReader) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamGenericReader) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(ReadableStreamDefaultReader, + ReadableStreamGenericReader, + mReadRequests) +NS_IMPL_ADDREF_INHERITED(ReadableStreamDefaultReader, + ReadableStreamGenericReader) +NS_IMPL_RELEASE_INHERITED(ReadableStreamDefaultReader, + ReadableStreamGenericReader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamDefaultReader) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(ReadableStreamGenericReader) + +ReadableStreamDefaultReader::ReadableStreamDefaultReader(nsISupports* aGlobal) + : ReadableStreamGenericReader(do_QueryInterface(aGlobal)) {} + +ReadableStreamDefaultReader::~ReadableStreamDefaultReader() { + mReadRequests.clear(); +} + +JSObject* ReadableStreamDefaultReader::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return ReadableStreamDefaultReader_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-stream-reader-generic-initialize +bool ReadableStreamReaderGenericInitialize(ReadableStreamGenericReader* aReader, + ReadableStream* aStream) { + // Step 1. + aReader->SetStream(aStream); + + // Step 2. + aStream->SetReader(aReader); + + aReader->SetClosedPromise( + Promise::CreateInfallible(aReader->GetParentObject())); + + switch (aStream->State()) { + // Step 3. + case ReadableStream::ReaderState::Readable: + // Step 3.1 + // Promise created above. + return true; + // Step 4. + case ReadableStream::ReaderState::Closed: + // Step 4.1. + aReader->ClosedPromise()->MaybeResolve(JS::UndefinedHandleValue); + + return true; + // Step 5. + case ReadableStream::ReaderState::Errored: { + // Step 5.1 Implicit + // Step 5.2 + JS::RootingContext* rcx = RootingCx(); + JS::Rooted<JS::Value> rootedError(rcx, aStream->StoredError()); + aReader->ClosedPromise()->MaybeReject(rootedError); + + // Step 5.3 + aReader->ClosedPromise()->SetSettledPromiseIsHandled(); + return true; + } + default: + MOZ_ASSERT_UNREACHABLE("Unknown ReaderState"); + return false; + } +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#default-reader-constructor && +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-reader +/* static */ +already_AddRefed<ReadableStreamDefaultReader> +ReadableStreamDefaultReader::Constructor(const GlobalObject& aGlobal, + ReadableStream& aStream, + ErrorResult& aRv) { + RefPtr<ReadableStreamDefaultReader> reader = + new ReadableStreamDefaultReader(aGlobal.GetAsSupports()); + + // https://streams.spec.whatwg.org/#set-up-readable-stream-default-reader + // Step 1. + if (aStream.Locked()) { + aRv.ThrowTypeError( + "Cannot create a new reader for a readable stream already locked by " + "another reader."); + return nullptr; + } + + // Step 2. + RefPtr<ReadableStream> streamPtr = &aStream; + if (!ReadableStreamReaderGenericInitialize(reader, streamPtr)) { + return nullptr; + } + + // Step 3. + reader->mReadRequests.clear(); + + return reader.forget(); +} + +void Read_ReadRequest::ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) { + // https://streams.spec.whatwg.org/#default-reader-read Step 3. + // chunk steps, given chunk: + // Step 1. Resolve promise with «[ "value" → chunk, "done" → false ]». + + // Value may need to be wrapped if stream and reader are in different + // compartments. + JS::Rooted<JS::Value> chunk(aCx, aChunk); + if (!JS_WrapValue(aCx, &chunk)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + RootedDictionary<ReadableStreamReadResult> result(aCx); + result.mValue = chunk; + result.mDone.Construct(false); + + // Ensure that the object is created with the current global. + JS::Rooted<JS::Value> value(aCx); + if (!ToJSValue(aCx, std::move(result), &value)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mPromise->MaybeResolve(value); +} + +void Read_ReadRequest::CloseSteps(JSContext* aCx, ErrorResult& aRv) { + // https://streams.spec.whatwg.org/#default-reader-read Step 3. + // close steps: + // Step 1. Resolve promise with «[ "value" → undefined, "done" → true ]». + RootedDictionary<ReadableStreamReadResult> result(aCx); + result.mValue.setUndefined(); + result.mDone.Construct(true); + + JS::Rooted<JS::Value> value(aCx); + if (!ToJSValue(aCx, std::move(result), &value)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mPromise->MaybeResolve(value); +} + +void Read_ReadRequest::ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e, + ErrorResult& aRv) { + // https://streams.spec.whatwg.org/#default-reader-read Step 3. + // error steps: + // Step 1. Reject promise with e. + mPromise->MaybeReject(e); +} + +NS_IMPL_CYCLE_COLLECTION(ReadRequest) +NS_IMPL_CYCLE_COLLECTION_INHERITED(Read_ReadRequest, ReadRequest, mPromise) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ReadRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ReadRequest) + +NS_IMPL_ADDREF_INHERITED(Read_ReadRequest, ReadRequest) +NS_IMPL_RELEASE_INHERITED(Read_ReadRequest, ReadRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadRequest) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Read_ReadRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadRequest) + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-stream-default-reader-read +void ReadableStreamDefaultReaderRead(JSContext* aCx, + ReadableStreamGenericReader* aReader, + ReadRequest* aRequest, ErrorResult& aRv) { + // Step 1. + ReadableStream* stream = aReader->GetStream(); + + // Step 2. + MOZ_ASSERT(stream); + + // Step 3. + stream->SetDisturbed(true); + + switch (stream->State()) { + // Step 4. + case ReadableStream::ReaderState::Closed: { + aRequest->CloseSteps(aCx, aRv); + return; + } + + case ReadableStream::ReaderState::Errored: { + JS::Rooted<JS::Value> storedError(aCx, stream->StoredError()); + aRequest->ErrorSteps(aCx, storedError, aRv); + return; + } + + case ReadableStream::ReaderState::Readable: { + RefPtr<ReadableStreamController> controller(stream->Controller()); + MOZ_ASSERT(controller); + controller->PullSteps(aCx, aRequest, aRv); + return; + } + } +} +} // namespace streams_abstract + +// Return a raw pointer here to avoid refcounting, but make sure it's safe +// (the object should be kept alive by the callee). +// https://streams.spec.whatwg.org/#default-reader-read +already_AddRefed<Promise> ReadableStreamDefaultReader::Read(ErrorResult& aRv) { + // Step 1. + if (!mStream) { + aRv.ThrowTypeError("Reading is not possible after calling releaseLock."); + return nullptr; + } + + // Step 2. + RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject()); + + // Step 3. + RefPtr<ReadRequest> request = new Read_ReadRequest(promise); + + // Step 4. + AutoEntryScript aes(mGlobal, "ReadableStreamDefaultReader::Read"); + JSContext* cx = aes.cx(); + + ReadableStreamDefaultReaderRead(cx, this, request, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 5. + return promise.forget(); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#readable-stream-reader-generic-release +void ReadableStreamReaderGenericRelease(ReadableStreamGenericReader* aReader, + ErrorResult& aRv) { + // Step 1. Let stream be reader.[[stream]]. + RefPtr<ReadableStream> stream = aReader->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Assert: stream.[[reader]] is reader. + MOZ_ASSERT(stream->GetReader() == aReader); + + // Step 4. If stream.[[state]] is "readable", reject reader.[[closedPromise]] + // with a TypeError exception. + if (stream->State() == ReadableStream::ReaderState::Readable) { + aReader->ClosedPromise()->MaybeRejectWithTypeError( + "Releasing lock on readable stream"); + } else { + // Step 5. Otherwise, set reader.[[closedPromise]] to a promise rejected + // with a TypeError exception. + RefPtr<Promise> promise = Promise::CreateRejectedWithTypeError( + aReader->GetParentObject(), "Lock Released"_ns, aRv); + aReader->SetClosedPromise(promise.forget()); + } + + // Step 6. Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + aReader->ClosedPromise()->SetSettledPromiseIsHandled(); + + // Step 7. Perform ! stream.[[controller]].[[ReleaseSteps]](). + stream->Controller()->ReleaseSteps(); + + // Step 8. Set stream.[[reader]] to undefined. + stream->SetReader(nullptr); + + // Step 9. Set reader.[[stream]] to undefined. + aReader->SetStream(nullptr); +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaultreadererrorreadrequests +void ReadableStreamDefaultReaderErrorReadRequests( + JSContext* aCx, ReadableStreamDefaultReader* aReader, + JS::Handle<JS::Value> aError, ErrorResult& aRv) { + // Step 1. Let readRequests be reader.[[readRequests]]. + LinkedList<RefPtr<ReadRequest>> readRequests = + std::move(aReader->ReadRequests()); + + // Step 2. Set reader.[[readRequests]] to a new empty list. + // Note: The std::move already cleared this anyway. + aReader->ReadRequests().clear(); + + // Step 3. For each readRequest of readRequests, + while (RefPtr<ReadRequest> readRequest = readRequests.popFirst()) { + // Step 3.1. Perform readRequest’s error steps, given e. + readRequest->ErrorSteps(aCx, aError, aRv); + if (aRv.Failed()) { + return; + } + } +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaultreaderrelease +void ReadableStreamDefaultReaderRelease(JSContext* aCx, + ReadableStreamDefaultReader* aReader, + ErrorResult& aRv) { + // Step 1. Perform ! ReadableStreamReaderGenericRelease(reader). + ReadableStreamReaderGenericRelease(aReader, aRv); + if (aRv.Failed()) { + return; + } + + // Step 2. Let e be a new TypeError exception. + ErrorResult rv; + rv.ThrowTypeError("Releasing lock"); + JS::Rooted<JS::Value> error(aCx); + MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), &error)); + + // Step 3. Perform ! ReadableStreamDefaultReaderErrorReadRequests(reader, e). + ReadableStreamDefaultReaderErrorReadRequests(aCx, aReader, error, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#default-reader-release-lock +void ReadableStreamDefaultReader::ReleaseLock(ErrorResult& aRv) { + // Step 1. If this.[[stream]] is undefined, return. + if (!mStream) { + return; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(mGlobal)) { + return aRv.ThrowUnknownError("Internal error"); + } + JSContext* cx = jsapi.cx(); + + // Step 2. Perform ! ReadableStreamDefaultReaderRelease(this). + RefPtr<ReadableStreamDefaultReader> thisRefPtr = this; + ReadableStreamDefaultReaderRelease(cx, thisRefPtr, aRv); +} + +// https://streams.spec.whatwg.org/#generic-reader-closed +already_AddRefed<Promise> ReadableStreamGenericReader::Closed() const { + // Step 1. + return do_AddRef(mClosedPromise); +} + +// https://streams.spec.whatwg.org/#readable-stream-reader-generic-cancel +MOZ_CAN_RUN_SCRIPT +static already_AddRefed<Promise> ReadableStreamGenericReaderCancel( + JSContext* aCx, ReadableStreamGenericReader* aReader, + JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1 (Strong ref for below call). + RefPtr<ReadableStream> stream = aReader->GetStream(); + + // Step 2. + MOZ_ASSERT(stream); + + // Step 3. + return ReadableStreamCancel(aCx, stream, aReason, aRv); +} + +already_AddRefed<Promise> ReadableStreamGenericReader::Cancel( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1. If this.[[stream]] is undefined, + // return a promise rejected with a TypeError exception. + if (!mStream) { + aRv.ThrowTypeError("Canceling is not possible after calling releaseLock."); + return nullptr; + } + + // Step 2. Return ! ReadableStreamReaderGenericCancel(this, reason). + return ReadableStreamGenericReaderCancel(aCx, this, aReason, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-reader +void SetUpReadableStreamDefaultReader(ReadableStreamDefaultReader* aReader, + ReadableStream* aStream, + ErrorResult& aRv) { + // Step 1. + if (IsReadableStreamLocked(aStream)) { + return aRv.ThrowTypeError( + "Cannot get a new reader for a readable stream already locked by " + "another reader."); + } + + // Step 2. + if (!ReadableStreamReaderGenericInitialize(aReader, aStream)) { + return; + } + + // Step 3. + aReader->ReadRequests().clear(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-a-chunk +// To read a chunk from a ReadableStreamDefaultReader reader, given a read +// request readRequest, perform ! ReadableStreamDefaultReaderRead(reader, +// readRequest). +void ReadableStreamDefaultReader::ReadChunk(JSContext* aCx, + ReadRequest& aRequest, + ErrorResult& aRv) { + ReadableStreamDefaultReaderRead(aCx, this, &aRequest, aRv); +} + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStreamDefaultReader.h b/dom/streams/ReadableStreamDefaultReader.h new file mode 100644 index 0000000000..d037b98cb7 --- /dev/null +++ b/dom/streams/ReadableStreamDefaultReader.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamDefaultReader_h +#define mozilla_dom_ReadableStreamDefaultReader_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ReadableStreamGenericReader.h" +#include "mozilla/dom/ReadRequest.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "mozilla/LinkedList.h" + +namespace mozilla::dom { + +class Promise; +class ReadableStream; + +// https://streams.spec.whatwg.org/#default-reader-read +struct Read_ReadRequest : public ReadRequest { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Read_ReadRequest, ReadRequest) + + RefPtr<Promise> mPromise; + + explicit Read_ReadRequest(Promise* aPromise) : mPromise(aPromise) {} + + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override; + + void CloseSteps(JSContext* aCx, ErrorResult& aRv) override; + + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e, + ErrorResult& aRv) override; + + protected: + ~Read_ReadRequest() override = default; +}; + +class ReadableStreamDefaultReader final : public ReadableStreamGenericReader, + public nsWrapperCache + +{ + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_INHERITED( + ReadableStreamDefaultReader, ReadableStreamGenericReader) + + public: + explicit ReadableStreamDefaultReader(nsISupports* aGlobal); + + protected: + ~ReadableStreamDefaultReader() override; + + public: + bool IsDefault() override { return true; } + bool IsBYOB() override { return false; } + ReadableStreamDefaultReader* AsDefault() override { return this; } + ReadableStreamBYOBReader* AsBYOB() override { + MOZ_CRASH(); + return nullptr; + } + + // Public functions to implement other specs + // https://streams.spec.whatwg.org/#other-specs-rs-create + + // The following algorithms can be used on arbitrary ReadableStream instances, + // including ones that are created by web developers. They can all fail in + // various operation-specific ways, and these failures should be handled by + // the calling specification. + + // https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-a-chunk + MOZ_CAN_RUN_SCRIPT void ReadChunk(JSContext* aCx, ReadRequest& aRequest, + ErrorResult& aRv); + + // IDL layer functions + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // IDL methods + + static already_AddRefed<ReadableStreamDefaultReader> Constructor( + const GlobalObject& aGlobal, ReadableStream& stream, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Read(ErrorResult& aRv); + + void ReleaseLock(ErrorResult& aRv); + + LinkedList<RefPtr<ReadRequest>>& ReadRequests() { return mReadRequests; } + + private: + LinkedList<RefPtr<ReadRequest>> mReadRequests = {}; +}; + +namespace streams_abstract { + +void SetUpReadableStreamDefaultReader(ReadableStreamDefaultReader* aReader, + ReadableStream* aStream, + ErrorResult& aRv); + +void ReadableStreamDefaultReaderErrorReadRequests( + JSContext* aCx, ReadableStreamDefaultReader* aReader, + JS::Handle<JS::Value> aError, ErrorResult& aRv); + +void ReadableStreamDefaultReaderRelease(JSContext* aCx, + ReadableStreamDefaultReader* aReader, + ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_ReadableStreamDefaultReader_h diff --git a/dom/streams/ReadableStreamGenericReader.h b/dom/streams/ReadableStreamGenericReader.h new file mode 100644 index 0000000000..ccc966bdc4 --- /dev/null +++ b/dom/streams/ReadableStreamGenericReader.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamGenericReader_h +#define mozilla_dom_ReadableStreamGenericReader_h + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ReadableStreamDefaultReaderBinding.h" +#include "nsISupports.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { + +class ReadableStream; +class ReadableStreamDefaultReader; +class ReadableStreamBYOBReader; + +// Base class for internal slots of readable stream readers +class ReadableStreamGenericReader : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ReadableStreamGenericReader) + + explicit ReadableStreamGenericReader(nsCOMPtr<nsIGlobalObject> aGlobal) + : mGlobal(std::move(aGlobal)) {} + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + virtual bool IsDefault() = 0; + virtual bool IsBYOB() = 0; + virtual ReadableStreamDefaultReader* AsDefault() = 0; + virtual ReadableStreamBYOBReader* AsBYOB() = 0; + + Promise* ClosedPromise() { return mClosedPromise; } + void SetClosedPromise(already_AddRefed<Promise>&& aClosedPromise) { + mClosedPromise = aClosedPromise; + } + + ReadableStream* GetStream() { return mStream; } + void SetStream(already_AddRefed<ReadableStream>&& aStream) { + mStream = aStream; + } + void SetStream(ReadableStream* aStream) { + RefPtr<ReadableStream> stream(aStream); + SetStream(stream.forget()); + } + + // IDL Methods + already_AddRefed<Promise> Closed() const; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Cancel( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv); + + protected: + virtual ~ReadableStreamGenericReader() = default; + + nsCOMPtr<nsIGlobalObject> mGlobal; + RefPtr<Promise> mClosedPromise; + RefPtr<ReadableStream> mStream; +}; + +namespace streams_abstract { + +bool ReadableStreamReaderGenericInitialize(ReadableStreamGenericReader* aReader, + ReadableStream* aStream); + +void ReadableStreamReaderGenericRelease(ReadableStreamGenericReader* aReader, + ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/ReadableStreamPipeTo.cpp b/dom/streams/ReadableStreamPipeTo.cpp new file mode 100644 index 0000000000..ed5db4f086 --- /dev/null +++ b/dom/streams/ReadableStreamPipeTo.cpp @@ -0,0 +1,977 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ReadableStreamPipeTo.h" + +#include "mozilla/dom/AbortFollower.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/WritableStream.h" +#include "mozilla/dom/WritableStreamDefaultWriter.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" + +#include "js/Exception.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +struct PipeToReadRequest; +class WriteFinishedPromiseHandler; +class ShutdownActionFinishedPromiseHandler; + +// https://streams.spec.whatwg.org/#readable-stream-pipe-to (Steps 14-15.) +// +// This class implements everything that is required to read all chunks from +// the reader (source) and write them to writer (destination), while +// following the constraints given in the spec using our implementation-defined +// behavior. +// +// The cycle-collected references look roughly like this: +// clang-format off +// +// Closed promise <-- ReadableStreamDefaultReader <--> ReadableStream +// | ^ | +// |(PromiseHandler) |(mReader) |(ReadRequest) +// | | | +// |-------------> PipeToPump <------- +// ^ | | +// |---------------| | | +// | | |-------(mLastWrite) --------> +// |(PromiseHandler) | |< ---- (PromiseHandler) ---- Promise +// | | ^ +// | |(mWriter) |(mWriteRequests) +// | v | +// Closed promise <-- WritableStreamDefaultWriter <--------> WritableStream +// +// clang-format on +class PipeToPump final : public AbortFollower { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(PipeToPump) + + friend struct PipeToReadRequest; + friend class WriteFinishedPromiseHandler; + friend class ShutdownActionFinishedPromiseHandler; + + PipeToPump(Promise* aPromise, ReadableStreamDefaultReader* aReader, + WritableStreamDefaultWriter* aWriter, bool aPreventClose, + bool aPreventAbort, bool aPreventCancel) + : mPromise(aPromise), + mReader(aReader), + mWriter(aWriter), + mPreventClose(aPreventClose), + mPreventAbort(aPreventAbort), + mPreventCancel(aPreventCancel) {} + + MOZ_CAN_RUN_SCRIPT void Start(JSContext* aCx, AbortSignal* aSignal); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void RunAbortAlgorithm() override; + + private: + ~PipeToPump() override = default; + + MOZ_CAN_RUN_SCRIPT void PerformAbortAlgorithm(JSContext* aCx, + AbortSignalImpl* aSignal); + + MOZ_CAN_RUN_SCRIPT bool SourceOrDestErroredOrClosed(JSContext* aCx); + + using ShutdownAction = already_AddRefed<Promise> (*)( + JSContext*, PipeToPump*, JS::Handle<mozilla::Maybe<JS::Value>>, + ErrorResult&); + + MOZ_CAN_RUN_SCRIPT void ShutdownWithAction( + JSContext* aCx, ShutdownAction aAction, + JS::Handle<mozilla::Maybe<JS::Value>> aError); + MOZ_CAN_RUN_SCRIPT void ShutdownWithActionAfterFinishedWrite( + JSContext* aCx, ShutdownAction aAction, + JS::Handle<mozilla::Maybe<JS::Value>> aError); + + MOZ_CAN_RUN_SCRIPT void Shutdown( + JSContext* aCx, JS::Handle<mozilla::Maybe<JS::Value>> aError); + + void Finalize(JSContext* aCx, JS::Handle<mozilla::Maybe<JS::Value>> aError); + + MOZ_CAN_RUN_SCRIPT void OnReadFulfilled(JSContext* aCx, + JS::Handle<JS::Value> aChunk, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void OnWriterReady(JSContext* aCx, JS::Handle<JS::Value>); + MOZ_CAN_RUN_SCRIPT void Read(JSContext* aCx); + + MOZ_CAN_RUN_SCRIPT void OnSourceClosed(JSContext* aCx, JS::Handle<JS::Value>); + MOZ_CAN_RUN_SCRIPT void OnSourceErrored( + JSContext* aCx, JS::Handle<JS::Value> aSourceStoredError); + + MOZ_CAN_RUN_SCRIPT void OnDestClosed(JSContext* aCx, JS::Handle<JS::Value>); + MOZ_CAN_RUN_SCRIPT void OnDestErrored(JSContext* aCx, + JS::Handle<JS::Value> aDestStoredError); + + RefPtr<Promise> mPromise; + RefPtr<ReadableStreamDefaultReader> mReader; + RefPtr<WritableStreamDefaultWriter> mWriter; + RefPtr<Promise> mLastWritePromise; + const bool mPreventClose; + const bool mPreventAbort; + const bool mPreventCancel; + bool mShuttingDown = false; +#ifdef DEBUG + bool mReadChunk = false; +#endif +}; + +// This is a helper class for PipeToPump that allows it to attach +// member functions as promise handlers. +class PipeToPumpHandler final : public PromiseNativeHandler { + virtual ~PipeToPumpHandler() = default; + + using FunPtr = void (PipeToPump::*)(JSContext*, JS::Handle<JS::Value>); + + RefPtr<PipeToPump> mPipeToPump; + FunPtr mResolved; + FunPtr mRejected; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(PipeToPumpHandler) + + explicit PipeToPumpHandler(PipeToPump* aPipeToPump, FunPtr aResolved, + FunPtr aRejected) + : mPipeToPump(aPipeToPump), mResolved(aResolved), mRejected(aRejected) {} + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult&) override { + if (mResolved) { + (mPipeToPump->*mResolved)(aCx, aValue); + } + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aReason, + ErrorResult&) override { + if (mRejected) { + (mPipeToPump->*mRejected)(aCx, aReason); + } + } +}; + +NS_IMPL_CYCLE_COLLECTION(PipeToPumpHandler, mPipeToPump) +NS_IMPL_CYCLE_COLLECTING_ADDREF(PipeToPumpHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PipeToPumpHandler) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PipeToPumpHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +void PipeToPump::RunAbortAlgorithm() { + AutoJSAPI jsapi; + if (!jsapi.Init(mReader->GetStream()->GetParentObject())) { + NS_WARNING( + "Failed to initialize AutoJSAPI in PipeToPump::RunAbortAlgorithm"); + return; + } + JSContext* cx = jsapi.cx(); + + RefPtr<AbortSignalImpl> signal = Signal(); + PerformAbortAlgorithm(cx, signal); +} + +void PipeToPump::PerformAbortAlgorithm(JSContext* aCx, + AbortSignalImpl* aSignal) { + MOZ_ASSERT(aSignal->Aborted()); + + // https://streams.spec.whatwg.org/#readable-stream-pipe-to + // Step 14.1. Let abortAlgorithm be the following steps: + // Note: All the following steps are 14.1.xx + + // Step 1. Let error be signal’s abort reason. + JS::Rooted<JS::Value> error(aCx); + aSignal->GetReason(aCx, &error); + + auto action = [](JSContext* aCx, PipeToPump* aPipeToPump, + JS::Handle<mozilla::Maybe<JS::Value>> aError, + ErrorResult& aRv) MOZ_CAN_RUN_SCRIPT { + JS::Rooted<JS::Value> error(aCx, *aError); + + // Step 2. Let actions be an empty ordered set. + nsTArray<RefPtr<Promise>> actions; + + // Step 3. If preventAbort is false, append the following action to actions: + if (!aPipeToPump->mPreventAbort) { + RefPtr<WritableStream> dest = aPipeToPump->mWriter->GetStream(); + + // Step 3.1. If dest.[[state]] is "writable", return ! + // WritableStreamAbort(dest, error). + if (dest->State() == WritableStream::WriterState::Writable) { + RefPtr<Promise> p = WritableStreamAbort(aCx, dest, error, aRv); + if (aRv.Failed()) { + return already_AddRefed<Promise>(); + } + actions.AppendElement(p); + } + + // Step 3.2. Otherwise, return a promise resolved with undefined. + // Note: This is basically a no-op. + } + + // Step 4. If preventCancel is false, append the following action action to + // actions: + if (!aPipeToPump->mPreventCancel) { + RefPtr<ReadableStream> source = aPipeToPump->mReader->GetStream(); + + // Step 4.1. If source.[[state]] is "readable", return ! + // ReadableStreamCancel(source, error). + if (source->State() == ReadableStream::ReaderState::Readable) { + RefPtr<Promise> p = ReadableStreamCancel(aCx, source, error, aRv); + if (aRv.Failed()) { + return already_AddRefed<Promise>(); + } + actions.AppendElement(p); + } + + // Step 4.2. Otherwise, return a promise resolved with undefined. + // No-op again. + } + + // Step 5. .. action consisting of getting a promise to wait for + // all of the actions in actions ... + return Promise::All(aCx, actions, aRv); + }; + + // Step 5. Shutdown with an action consisting of getting a promise to wait for + // all of the actions in actions, and with error. + JS::Rooted<Maybe<JS::Value>> someError(aCx, Some(error.get())); + ShutdownWithAction(aCx, action, someError); +} + +bool PipeToPump::SourceOrDestErroredOrClosed(JSContext* aCx) { + // (Constraint) Error and close states must be propagated: + // the following conditions must be applied in order. + RefPtr<ReadableStream> source = mReader->GetStream(); + RefPtr<WritableStream> dest = mWriter->GetStream(); + + // Step 1. Errors must be propagated forward: if source.[[state]] is or + // becomes "errored", then + if (source->State() == ReadableStream::ReaderState::Errored) { + JS::Rooted<JS::Value> storedError(aCx, source->StoredError()); + OnSourceErrored(aCx, storedError); + return true; + } + + // Step 2. Errors must be propagated backward: if dest.[[state]] is or becomes + // "errored", then + if (dest->State() == WritableStream::WriterState::Errored) { + JS::Rooted<JS::Value> storedError(aCx, dest->StoredError()); + OnDestErrored(aCx, storedError); + return true; + } + + // Step 3. Closing must be propagated forward: if source.[[state]] is or + // becomes "closed", then + if (source->State() == ReadableStream::ReaderState::Closed) { + OnSourceClosed(aCx, JS::UndefinedHandleValue); + return true; + } + + // Step 4. Closing must be propagated backward: + // if ! WritableStreamCloseQueuedOrInFlight(dest) is true + // or dest.[[state]] is "closed", then + if (dest->CloseQueuedOrInFlight() || + dest->State() == WritableStream::WriterState::Closed) { + OnDestClosed(aCx, JS::UndefinedHandleValue); + return true; + } + + return false; +} + +// https://streams.spec.whatwg.org/#readable-stream-pipe-to +// Steps 14-15. +void PipeToPump::Start(JSContext* aCx, AbortSignal* aSignal) { + // Step 14. If signal is not undefined, + if (aSignal) { + // Step 14.1. Let abortAlgorithm be the following steps: + // ... This is implemented by RunAbortAlgorithm. + + // Step 14.2. If signal is aborted, perform abortAlgorithm and + // return promise. + if (aSignal->Aborted()) { + PerformAbortAlgorithm(aCx, aSignal); + return; + } + + // Step 14.3. Add abortAlgorithm to signal. + Follow(aSignal); + } + + // Step 15. In parallel but not really; see #905, using reader and writer, + // read all chunks from source and write them to dest. + // Due to the locking provided by the reader and writer, + // the exact manner in which this happens is not observable to author code, + // and so there is flexibility in how this is done. + + // (Constraint) Error and close states must be propagated + + // Before piping has started, we have to check for source/destination being + // errored/closed manually. + if (SourceOrDestErroredOrClosed(aCx)) { + return; + } + + // We use the following two promises to propagate error and close states + // during piping. + RefPtr<Promise> readerClosed = mReader->ClosedPromise(); + readerClosed->AppendNativeHandler(new PipeToPumpHandler( + this, &PipeToPump::OnSourceClosed, &PipeToPump::OnSourceErrored)); + + // Note: Because we control the destination/writer it should never be closed + // after we did the initial check above with SourceOrDestErroredOrClosed. + RefPtr<Promise> writerClosed = mWriter->ClosedPromise(); + writerClosed->AppendNativeHandler(new PipeToPumpHandler( + this, &PipeToPump::OnDestClosed, &PipeToPump::OnDestErrored)); + + Read(aCx); +} + +class WriteFinishedPromiseHandler final : public PromiseNativeHandler { + RefPtr<PipeToPump> mPipeToPump; + PipeToPump::ShutdownAction mAction; + bool mHasError; + JS::Heap<JS::Value> mError; + + virtual ~WriteFinishedPromiseHandler() { mozilla::DropJSObjects(this); }; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WriteFinishedPromiseHandler) + + explicit WriteFinishedPromiseHandler( + JSContext* aCx, PipeToPump* aPipeToPump, + PipeToPump::ShutdownAction aAction, + JS::Handle<mozilla::Maybe<JS::Value>> aError) + : mPipeToPump(aPipeToPump), mAction(aAction) { + mHasError = aError.isSome(); + if (mHasError) { + mError = *aError; + } + mozilla::HoldJSObjects(this); + } + + MOZ_CAN_RUN_SCRIPT void WriteFinished(JSContext* aCx) { + RefPtr<PipeToPump> pipeToPump = mPipeToPump; // XXX known-live? + JS::Rooted<Maybe<JS::Value>> error(aCx); + if (mHasError) { + error = Some(mError); + } + pipeToPump->ShutdownWithActionAfterFinishedWrite(aCx, mAction, error); + } + + MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult&) override { + WriteFinished(aCx); + } + + MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aReason, + ErrorResult&) override { + WriteFinished(aCx); + } +}; + +NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(WriteFinishedPromiseHandler, + (mPipeToPump), (mError)) +NS_IMPL_CYCLE_COLLECTING_ADDREF(WriteFinishedPromiseHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WriteFinishedPromiseHandler) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WriteFinishedPromiseHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +// https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action +// Shutdown with an action: if any of the above requirements ask to shutdown +// with an action action, optionally with an error originalError, then: +void PipeToPump::ShutdownWithAction( + JSContext* aCx, ShutdownAction aAction, + JS::Handle<mozilla::Maybe<JS::Value>> aError) { + // Step 1. If shuttingDown is true, abort these substeps. + if (mShuttingDown) { + return; + } + + // Step 2. Set shuttingDown to true. + mShuttingDown = true; + + // Step 3. If dest.[[state]] is "writable" and ! + // WritableStreamCloseQueuedOrInFlight(dest) is false, + RefPtr<WritableStream> dest = mWriter->GetStream(); + if (dest->State() == WritableStream::WriterState::Writable && + !dest->CloseQueuedOrInFlight()) { + // Step 3.1. If any chunks have been read but not yet written, write them to + // dest. + // Step 3.2. Wait until every chunk that has been read has been + // written (i.e. the corresponding promises have settled). + // + // Note: Write requests are processed in order, so when the promise + // for the last written chunk is settled all previous chunks have been + // written as well. + if (mLastWritePromise) { + mLastWritePromise->AppendNativeHandler( + new WriteFinishedPromiseHandler(aCx, this, aAction, aError)); + return; + } + } + + // Don't have to wait for last write, immediately continue. + ShutdownWithActionAfterFinishedWrite(aCx, aAction, aError); +} + +class ShutdownActionFinishedPromiseHandler final : public PromiseNativeHandler { + RefPtr<PipeToPump> mPipeToPump; + bool mHasError; + JS::Heap<JS::Value> mError; + + virtual ~ShutdownActionFinishedPromiseHandler() { + mozilla::DropJSObjects(this); + } + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS( + ShutdownActionFinishedPromiseHandler) + + explicit ShutdownActionFinishedPromiseHandler( + JSContext* aCx, PipeToPump* aPipeToPump, + JS::Handle<mozilla::Maybe<JS::Value>> aError) + : mPipeToPump(aPipeToPump) { + mHasError = aError.isSome(); + if (mHasError) { + mError = *aError; + } + mozilla::HoldJSObjects(this); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult&) override { + // https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action + // Step 5. Upon fulfillment of p, finalize, passing along originalError if + // it was given. + JS::Rooted<Maybe<JS::Value>> error(aCx); + if (mHasError) { + error = Some(mError); + } + mPipeToPump->Finalize(aCx, error); + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aReason, + ErrorResult&) override { + // https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action + // Step 6. Upon rejection of p with reason newError, finalize with + // newError. + JS::Rooted<Maybe<JS::Value>> error(aCx, Some(aReason)); + mPipeToPump->Finalize(aCx, error); + } +}; + +NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(ShutdownActionFinishedPromiseHandler, + (mPipeToPump), (mError)) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ShutdownActionFinishedPromiseHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ShutdownActionFinishedPromiseHandler) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ShutdownActionFinishedPromiseHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +// https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action +// Continuation after Step 3. triggered a promise resolution. +void PipeToPump::ShutdownWithActionAfterFinishedWrite( + JSContext* aCx, ShutdownAction aAction, + JS::Handle<mozilla::Maybe<JS::Value>> aError) { + if (!aAction) { + // Used to implement shutdown without action. Finalize immediately. + Finalize(aCx, aError); + return; + } + + // Step 4. Let p be the result of performing action. + RefPtr<PipeToPump> thisRefPtr = this; + ErrorResult rv; + RefPtr<Promise> p = aAction(aCx, thisRefPtr, aError, rv); + + // Error while calling actions above, continue immediately with finalization. + if (rv.MaybeSetPendingException(aCx)) { + JS::Rooted<Maybe<JS::Value>> someError(aCx); + + JS::Rooted<JS::Value> error(aCx); + if (JS_GetPendingException(aCx, &error)) { + someError = Some(error.get()); + } + + JS_ClearPendingException(aCx); + + Finalize(aCx, someError); + return; + } + + // Steps 5-6. + p->AppendNativeHandler( + new ShutdownActionFinishedPromiseHandler(aCx, this, aError)); +} + +// https://streams.spec.whatwg.org/#rs-pipeTo-shutdown +// Shutdown: if any of the above requirements or steps ask to shutdown, +// optionally with an error error, then: +void PipeToPump::Shutdown(JSContext* aCx, + JS::Handle<mozilla::Maybe<JS::Value>> aError) { + // Note: We implement "shutdown" in terms of "shutdown with action". + // We can observe that when passing along an action that always succeeds + // shutdown with action and shutdown have the same behavior, when + // Ignoring the potential micro task for the promise that we skip anyway. + ShutdownWithAction(aCx, nullptr, aError); +} + +// https://streams.spec.whatwg.org/#rs-pipeTo-finalize +// Finalize: both forms of shutdown will eventually ask to finalize, +// optionally with an error error, which means to perform the following steps: +void PipeToPump::Finalize(JSContext* aCx, + JS::Handle<mozilla::Maybe<JS::Value>> aError) { + IgnoredErrorResult rv; + // Step 1. Perform ! WritableStreamDefaultWriterRelease(writer). + WritableStreamDefaultWriterRelease(aCx, mWriter); + + // Step 2. If reader implements ReadableStreamBYOBReader, + // perform ! ReadableStreamBYOBReaderRelease(reader). + // Note: We always use a default reader. + MOZ_ASSERT(!mReader->IsBYOB()); + + // Step 3. Otherwise, perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReaderRelease(aCx, mReader, rv); + NS_WARNING_ASSERTION(!rv.Failed(), + "ReadableStreamReaderGenericRelease should not fail."); + + // Step 3. If signal is not undefined, remove abortAlgorithm from signal. + if (IsFollowing()) { + Unfollow(); + } + + // Step 4. If error was given, reject promise with error. + if (aError.isSome()) { + JS::Rooted<JS::Value> error(aCx, *aError); + mPromise->MaybeReject(error); + } else { + // Step 5. Otherwise, resolve promise with undefined. + mPromise->MaybeResolveWithUndefined(); + } + + // Remove all references. + mPromise = nullptr; + mReader = nullptr; + mWriter = nullptr; + mLastWritePromise = nullptr; + Unfollow(); +} + +void PipeToPump::OnReadFulfilled(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) { + // (Constraint) 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). + // + // TODO: Implement the eventual resolution from + // https://github.com/whatwg/streams/issues/1207 + if (mShuttingDown) { + return; + } + + // Write asynchronously. Roughly this is like: + // `Promise.resolve().then(() => stream.write(chunk));` + // XXX: The spec currently does not require asynchronicity, but this still + // matches other engines' behavior. See + // https://github.com/whatwg/streams/issues/1243. + RefPtr<Promise> promise = + Promise::CreateInfallible(xpc::CurrentNativeGlobal(aCx)); + promise->MaybeResolveWithUndefined(); + auto result = promise->ThenWithCycleCollectedArgsJS( + [](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv, + const RefPtr<PipeToPump>& aSelf, + const RefPtr<WritableStreamDefaultWriter>& aWriter, + JS::Handle<JS::Value> aChunk) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> already_AddRefed<Promise> { + RefPtr<Promise> promise = + WritableStreamDefaultWriterWrite(aCx, aWriter, aChunk, aRv); + + // Last read has finished, so it's time to start reading again. + aSelf->Read(aCx); + + return promise.forget(); + }, + std::make_tuple(RefPtr{this}, mWriter), std::make_tuple(aChunk)); + if (result.isErr()) { + mLastWritePromise = nullptr; + return; + } + mLastWritePromise = result.unwrap(); + + mLastWritePromise->AppendNativeHandler( + new PipeToPumpHandler(this, nullptr, &PipeToPump::OnDestErrored)); +} + +void PipeToPump::OnWriterReady(JSContext* aCx, JS::Handle<JS::Value>) { + // Writer is ready again (i.e. backpressure was resolved), so read. + Read(aCx); +} + +struct PipeToReadRequest : public ReadRequest { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PipeToReadRequest, ReadRequest) + + RefPtr<PipeToPump> mPipeToPump; + + explicit PipeToReadRequest(PipeToPump* aPipeToPump) + : mPipeToPump(aPipeToPump) {} + + MOZ_CAN_RUN_SCRIPT void ChunkSteps(JSContext* aCx, + JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + RefPtr<PipeToPump> pipeToPump = mPipeToPump; // XXX known live? + pipeToPump->OnReadFulfilled(aCx, aChunk, aRv); + } + + // The reader's closed promise handlers will already call OnSourceClosed/ + // OnSourceErrored, so these steps can just be ignored. + void CloseSteps(JSContext* aCx, ErrorResult& aRv) override {} + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) override {} + + protected: + virtual ~PipeToReadRequest() = default; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PipeToReadRequest, ReadRequest, mPipeToPump) + +NS_IMPL_ADDREF_INHERITED(PipeToReadRequest, ReadRequest) +NS_IMPL_RELEASE_INHERITED(PipeToReadRequest, ReadRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PipeToReadRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadRequest) + +void PipeToPump::Read(JSContext* aCx) { +#ifdef DEBUG + mReadChunk = true; +#endif + + // (Constraint) Shutdown must stop activity: + // If shuttingDown becomes true, the user agent must not initiate + // further reads from reader + if (mShuttingDown) { + return; + } + + // (Constraint) Backpressure must be enforced: + // While WritableStreamDefaultWriterGetDesiredSize(writer) is ≤ 0 or is null, + // the user agent must not read from reader. + Nullable<double> desiredSize = + WritableStreamDefaultWriterGetDesiredSize(mWriter); + if (desiredSize.IsNull()) { + // This means the writer has errored. This is going to be handled + // by the writer closed promise. + return; + } + + if (desiredSize.Value() <= 0) { + // Wait for the writer to become ready before reading more data from + // the reader. We don't care about rejections here, because those are + // already handled by the writer closed promise. + RefPtr<Promise> readyPromise = mWriter->Ready(); + readyPromise->AppendNativeHandler( + new PipeToPumpHandler(this, &PipeToPump::OnWriterReady, nullptr)); + return; + } + + RefPtr<ReadableStreamDefaultReader> reader = mReader; + RefPtr<ReadRequest> request = new PipeToReadRequest(this); + ErrorResult rv; + ReadableStreamDefaultReaderRead(aCx, reader, request, rv); + if (rv.MaybeSetPendingException(aCx)) { + // XXX It's actually not quite obvious what we should do here. + // We've got an error during reading, so on the surface it seems logical + // to invoke `OnSourceErrored`. However in certain cases the required + // condition > source.[[state]] is or becomes "errored" < won't actually + // happen i.e. when `WritableStreamDefaultWriterWrite` called from + // `OnReadFulfilled` (via PipeToReadRequest::ChunkSteps) fails in + // a synchronous fashion. + JS::Rooted<JS::Value> error(aCx); + JS::Rooted<Maybe<JS::Value>> someError(aCx); + + // The error was moved to the JSContext by MaybeSetPendingException. + if (JS_GetPendingException(aCx, &error)) { + someError = Some(error.get()); + } + + JS_ClearPendingException(aCx); + + Shutdown(aCx, someError); + } +} + +// Step 3. Closing must be propagated forward: if source.[[state]] is or +// becomes "closed", then +void PipeToPump::OnSourceClosed(JSContext* aCx, JS::Handle<JS::Value>) { + // Step 3.1. If preventClose is false, shutdown with an action of + // ! WritableStreamDefaultWriterCloseWithErrorPropagation(writer). + if (!mPreventClose) { + ShutdownWithAction( + aCx, + [](JSContext* aCx, PipeToPump* aPipeToPump, + JS::Handle<mozilla::Maybe<JS::Value>> aError, ErrorResult& aRv) + MOZ_CAN_RUN_SCRIPT { + RefPtr<WritableStreamDefaultWriter> writer = aPipeToPump->mWriter; + return WritableStreamDefaultWriterCloseWithErrorPropagation( + aCx, writer, aRv); + }, + JS::NothingHandleValue); + } else { + // Step 3.2 Otherwise, shutdown. + Shutdown(aCx, JS::NothingHandleValue); + } +} + +// Step 1. Errors must be propagated forward: if source.[[state]] is or +// becomes "errored", then +void PipeToPump::OnSourceErrored(JSContext* aCx, + JS::Handle<JS::Value> aSourceStoredError) { + // 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. + // + // TODO: Implement the eventual resolution from + // https://github.com/whatwg/streams/issues/1207 + + // Step 1.1 If preventAbort is false, shutdown with an action of + // ! WritableStreamAbort(dest, source.[[storedError]]) + // and with source.[[storedError]]. + JS::Rooted<Maybe<JS::Value>> error(aCx, Some(aSourceStoredError)); + if (!mPreventAbort) { + ShutdownWithAction( + aCx, + [](JSContext* aCx, PipeToPump* aPipeToPump, + JS::Handle<mozilla::Maybe<JS::Value>> aError, ErrorResult& aRv) + MOZ_CAN_RUN_SCRIPT { + JS::Rooted<JS::Value> error(aCx, *aError); + RefPtr<WritableStream> dest = aPipeToPump->mWriter->GetStream(); + return WritableStreamAbort(aCx, dest, error, aRv); + }, + error); + } else { + // Step 1.1. Otherwise, shutdown with source.[[storedError]]. + Shutdown(aCx, error); + } +} + +// Step 4. Closing must be propagated backward: +// if ! WritableStreamCloseQueuedOrInFlight(dest) is true +// or dest.[[state]] is "closed", then +void PipeToPump::OnDestClosed(JSContext* aCx, JS::Handle<JS::Value>) { + // Step 4.1. Assert: no chunks have been read or written. + // Note: No reading automatically implies no writing. + // In a perfect world OnDestClosed would only be called before we start + // piping, because afterwards the writer has an exclusive lock on the stream. + // In reality the closed promise can still be resolved after we release + // the lock on the writer in Finalize. + if (mShuttingDown) { + return; + } + MOZ_ASSERT(!mReadChunk); + + // Step 4.2. Let destClosed be a new TypeError. + JS::Rooted<Maybe<JS::Value>> destClosed(aCx, Nothing()); + { + ErrorResult rv; + rv.ThrowTypeError("Cannot pipe to closed stream"); + JS::Rooted<JS::Value> error(aCx); + bool ok = ToJSValue(aCx, std::move(rv), &error); + MOZ_RELEASE_ASSERT(ok, "must be ok"); + destClosed = Some(error.get()); + } + + // Step 4.3. If preventCancel is false, shutdown with an action of + // ! ReadableStreamCancel(source, destClosed) and with destClosed. + if (!mPreventCancel) { + ShutdownWithAction( + aCx, + [](JSContext* aCx, PipeToPump* aPipeToPump, + JS::Handle<mozilla::Maybe<JS::Value>> aError, ErrorResult& aRv) + MOZ_CAN_RUN_SCRIPT { + JS::Rooted<JS::Value> error(aCx, *aError); + RefPtr<ReadableStream> dest = aPipeToPump->mReader->GetStream(); + return ReadableStreamCancel(aCx, dest, error, aRv); + }, + destClosed); + } else { + // Step 4.4. Otherwise, shutdown with destClosed. + Shutdown(aCx, destClosed); + } +} + +// Step 2. Errors must be propagated backward: if dest.[[state]] is or becomes +// "errored", then +void PipeToPump::OnDestErrored(JSContext* aCx, + JS::Handle<JS::Value> aDestStoredError) { + // Step 2.1. If preventCancel is false, shutdown with an action of + // ! ReadableStreamCancel(source, dest.[[storedError]]) + // and with dest.[[storedError]]. + JS::Rooted<Maybe<JS::Value>> error(aCx, Some(aDestStoredError)); + if (!mPreventCancel) { + ShutdownWithAction( + aCx, + [](JSContext* aCx, PipeToPump* aPipeToPump, + JS::Handle<mozilla::Maybe<JS::Value>> aError, ErrorResult& aRv) + MOZ_CAN_RUN_SCRIPT { + JS::Rooted<JS::Value> error(aCx, *aError); + RefPtr<ReadableStream> dest = aPipeToPump->mReader->GetStream(); + return ReadableStreamCancel(aCx, dest, error, aRv); + }, + error); + } else { + // Step 2.1. Otherwise, shutdown with dest.[[storedError]]. + Shutdown(aCx, error); + } +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(PipeToPump) +NS_IMPL_CYCLE_COLLECTING_ADDREF(PipeToPump) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PipeToPump) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PipeToPump) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PipeToPump) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWriter) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastWritePromise) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PipeToPump) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReader) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWriter) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastWritePromise) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#readable-stream-pipe-to +already_AddRefed<Promise> ReadableStreamPipeTo( + ReadableStream* aSource, WritableStream* aDest, bool aPreventClose, + bool aPreventAbort, bool aPreventCancel, AbortSignal* aSignal, + mozilla::ErrorResult& aRv) { + // Step 1. Assert: source implements ReadableStream. (Implicit) + // Step 2. Assert: dest implements WritableStream. (Implicit) + // Step 3. Assert: preventClose, preventAbort, and preventCancel are all + // booleans (Implicit) + // Step 4. If signal was not given, let signal be + // undefined. (Implicit) + // Step 5. Assert: either signal is undefined, or signal + // implements AbortSignal. (Implicit) + // Step 6. Assert: !IsReadableStreamLocked(source) is false. + MOZ_ASSERT(!IsReadableStreamLocked(aSource)); + + // Step 7. Assert: !IsWritableStreamLocked(dest) is false. + MOZ_ASSERT(!IsWritableStreamLocked(aDest)); + + AutoJSAPI jsapi; + if (!jsapi.Init(aSource->GetParentObject())) { + aRv.ThrowUnknownError("Internal error"); + return nullptr; + } + JSContext* cx = jsapi.cx(); + + // Step 8. If source.[[controller]] implements ReadableByteStreamController, + // let reader be either !AcquireReadableStreamBYOBReader(source) or + // !AcquireReadableStreamDefaultReader(source), at the user agent’s + // discretion. + // Step 9. Otherwise, let reader be + // !AcquireReadableStreamDefaultReader(source). + + // Note: In the interests of simplicity, we choose here to always acquire + // a default reader. + RefPtr<ReadableStreamDefaultReader> reader = + AcquireReadableStreamDefaultReader(aSource, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 10. Let writer be ! AcquireWritableStreamDefaultWriter(dest). + RefPtr<WritableStreamDefaultWriter> writer = + AcquireWritableStreamDefaultWriter(aDest, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 11. Set source.[[disturbed]] to true. + aSource->SetDisturbed(true); + + // Step 12. Let shuttingDown be false. + // Note: PipeToPump ensures this by construction. + + // Step 13. Let promise be a new promise. + RefPtr<Promise> promise = + Promise::CreateInfallible(aSource->GetParentObject()); + + // Steps 14-15. + RefPtr<PipeToPump> pump = new PipeToPump( + promise, reader, writer, aPreventClose, aPreventAbort, aPreventCancel); + pump->Start(cx, aSignal); + + // Step 16. Return promise. + return promise.forget(); +} +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStreamPipeTo.h b/dom/streams/ReadableStreamPipeTo.h new file mode 100644 index 0000000000..6c92cb8c32 --- /dev/null +++ b/dom/streams/ReadableStreamPipeTo.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamPipeTo_h +#define mozilla_dom_ReadableStreamPipeTo_h + +#include "mozilla/Attributes.h" +#include "mozilla/AlreadyAddRefed.h" + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class AbortSignal; +class Promise; +class ReadableStream; +class WritableStream; + +namespace streams_abstract { +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> ReadableStreamPipeTo( + ReadableStream* aSource, WritableStream* aDest, bool aPreventClose, + bool aPreventAbort, bool aPreventCancel, AbortSignal* aSignal, + ErrorResult& aRv); +} + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ReadableStreamPipeTo_h diff --git a/dom/streams/ReadableStreamTee.cpp b/dom/streams/ReadableStreamTee.cpp new file mode 100644 index 0000000000..9bb30d8859 --- /dev/null +++ b/dom/streams/ReadableStreamTee.cpp @@ -0,0 +1,1011 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ReadableStreamTee.h" + +#include "ReadIntoRequest.h" +#include "TeeState.h" +#include "js/Exception.h" +#include "js/TypeDecls.h" +#include "js/experimental/TypedData.h" +#include "mozilla/dom/ByteStreamHelpers.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamBYOBReader.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/ReadableStreamGenericReader.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/ReadableByteStreamController.h" +#include "mozilla/dom/UnderlyingSourceBinding.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/CycleCollectedJSContext.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ReadableStreamDefaultTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase, mTeeState) +NS_IMPL_ADDREF_INHERITED(ReadableStreamDefaultTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(ReadableStreamDefaultTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + ReadableStreamDefaultTeeSourceAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsBase) + +already_AddRefed<Promise> +ReadableStreamDefaultTeeSourceAlgorithms::PullCallback( + JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = aController.GetParentObject(); + mTeeState->PullCallback(aCx, global, aRv); + if (!aRv.Failed()) { + return Promise::CreateResolvedWithUndefined(global, aRv); + } + return nullptr; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(ReadableStreamDefaultTeeReadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + ReadableStreamDefaultTeeReadRequest, ReadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTeeState) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + ReadableStreamDefaultTeeReadRequest, ReadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTeeState) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(ReadableStreamDefaultTeeReadRequest, ReadRequest) +NS_IMPL_RELEASE_INHERITED(ReadableStreamDefaultTeeReadRequest, ReadRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReadableStreamDefaultTeeReadRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadRequest) + +void ReadableStreamDefaultTeeReadRequest::ChunkSteps( + JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1. + class ReadableStreamDefaultTeeReadRequestChunkSteps + : public MicroTaskRunnable { + // Virtually const, but is cycle collected + MOZ_KNOWN_LIVE RefPtr<TeeState> mTeeState; + JS::PersistentRooted<JS::Value> mChunk; + + public: + ReadableStreamDefaultTeeReadRequestChunkSteps(JSContext* aCx, + TeeState* aTeeState, + JS::Handle<JS::Value> aChunk) + : mTeeState(aTeeState), mChunk(aCx, aChunk) {} + + MOZ_CAN_RUN_SCRIPT + void Run(AutoSlowOperation& aAso) override { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mTeeState->GetStream()->GetParentObject()))) { + return; + } + JSContext* cx = jsapi.cx(); + // Step Numbering below is relative to Chunk steps Microtask: + // + // Step 1. + mTeeState->SetReadAgain(false); + + // Step 2. + JS::Rooted<JS::Value> chunk1(cx, mChunk); + JS::Rooted<JS::Value> chunk2(cx, mChunk); + + // Step 3. Skipped until we implement cloneForBranch2 path. + MOZ_RELEASE_ASSERT(!mTeeState->CloneForBranch2()); + + // Step 4. + if (!mTeeState->Canceled1()) { + IgnoredErrorResult rv; + // Since we controlled the creation of the two stream branches, we know + // they both have default controllers. + RefPtr<ReadableStreamDefaultController> controller( + mTeeState->Branch1()->DefaultController()); + ReadableStreamDefaultControllerEnqueue(cx, controller, chunk1, rv); + (void)NS_WARN_IF(rv.Failed()); + } + + // Step 5. + if (!mTeeState->Canceled2()) { + IgnoredErrorResult rv; + RefPtr<ReadableStreamDefaultController> controller( + mTeeState->Branch2()->DefaultController()); + ReadableStreamDefaultControllerEnqueue(cx, controller, chunk2, rv); + (void)NS_WARN_IF(rv.Failed()); + } + + // Step 6. + mTeeState->SetReading(false); + + // Step 7. If |readAgain| is true, perform |pullAlgorithm|. + if (mTeeState->ReadAgain()) { + IgnoredErrorResult rv; + nsCOMPtr<nsIGlobalObject> global( + mTeeState->GetStream()->GetParentObject()); + mTeeState->PullCallback(cx, global, rv); + (void)NS_WARN_IF(rv.Failed()); + } + } + + bool Suppressed() override { + nsIGlobalObject* global = mTeeState->GetStream()->GetParentObject(); + return global && global->IsInSyncOperation(); + } + }; + + RefPtr<ReadableStreamDefaultTeeReadRequestChunkSteps> task = + MakeRefPtr<ReadableStreamDefaultTeeReadRequestChunkSteps>(aCx, mTeeState, + aChunk); + CycleCollectedJSContext::Get()->DispatchToMicroTask(task.forget()); +} + +void ReadableStreamDefaultTeeReadRequest::CloseSteps(JSContext* aCx, + ErrorResult& aRv) { + // Step Numbering below is relative to 'close steps' of + // https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee + // + // Step 1. + mTeeState->SetReading(false); + + // Step 2. + if (!mTeeState->Canceled1()) { + RefPtr<ReadableStreamDefaultController> controller( + mTeeState->Branch1()->DefaultController()); + ReadableStreamDefaultControllerClose(aCx, controller, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 3. + if (!mTeeState->Canceled2()) { + RefPtr<ReadableStreamDefaultController> controller( + mTeeState->Branch2()->DefaultController()); + ReadableStreamDefaultControllerClose(aCx, controller, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 4. + if (!mTeeState->Canceled1() || !mTeeState->Canceled2()) { + mTeeState->CancelPromise()->MaybeResolveWithUndefined(); + } +} + +void ReadableStreamDefaultTeeReadRequest::ErrorSteps( + JSContext* aCx, JS::Handle<JS::Value> aError, ErrorResult& aRv) { + mTeeState->SetReading(false); +} + +MOZ_CAN_RUN_SCRIPT void PullWithDefaultReader(JSContext* aCx, + TeeState* aTeeState, + ErrorResult& aRv); +MOZ_CAN_RUN_SCRIPT void PullWithBYOBReader(JSContext* aCx, TeeState* aTeeState, + JS::Handle<JSObject*> aView, + TeeBranch aForBranch, + ErrorResult& aRv); + +// Algorithm described in +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee, Steps +// 17 and Steps 18, genericized across branch numbers: +// +// Note: As specified this algorithm always returns a promise resolved with +// undefined, however as some places immediately discard said promise, we +// provide this version which doesn't return a promise. +// +// NativeByteStreamTeePullAlgorithm, which implements +// UnderlyingSourcePullCallbackHelper is the version which provies the return +// promise. +MOZ_CAN_RUN_SCRIPT void ByteStreamTeePullAlgorithm(JSContext* aCx, + TeeBranch aForBranch, + TeeState* aTeeState, + ErrorResult& aRv) { + // Step {17,18}.1: If reading is true, + if (aTeeState->Reading()) { + // Step {17,18}.1.1: Set readAgainForBranch{1,2} to true. + aTeeState->SetReadAgainForBranch(aForBranch, true); + + // Step {17,18}.1.1: Return a promise resolved with undefined. + return; + } + + // Step {17,18}.2: Set reading to true. + aTeeState->SetReading(true); + + // Step {17,18}.3: Let byobRequest be + // !ReadableByteStreamControllerGetBYOBRequest(branch{1,2}.[[controller]]). + RefPtr<ReadableStreamBYOBRequest> byobRequest = + ReadableByteStreamControllerGetBYOBRequest( + aCx, aTeeState->Branch(aForBranch)->Controller()->AsByte(), aRv); + if (aRv.Failed()) { + return; + } + + // Step {17,18}.4: If byobRequest is null, perform pullWithDefaultReader. + if (!byobRequest) { + PullWithDefaultReader(aCx, aTeeState, aRv); + } else { + // Step {17,18}.5: Otherwise, perform pullWithBYOBReader, given + // byobRequest.[[view]] and {false, true}. + JS::Rooted<JSObject*> view(aCx, byobRequest->View()); + PullWithBYOBReader(aCx, aTeeState, view, aForBranch, aRv); + } + + // Step {17,18}.6: Return a promise resolved with undefined. +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee +class ByteStreamTeeSourceAlgorithms final + : public UnderlyingSourceAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ByteStreamTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) + + ByteStreamTeeSourceAlgorithms(TeeState* aTeeState, TeeBranch aBranch) + : mTeeState(aTeeState), mBranch(aBranch) {} + + MOZ_CAN_RUN_SCRIPT void StartCallback(JSContext* aCx, + ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override { + // Step 21: Let startAlgorithm be an algorithm that returns undefined. + aRetVal.setUndefined(); + } + + // Step 17, 18 + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PullCallback( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) override { + // Step 1 - 5 + ByteStreamTeePullAlgorithm(aCx, mBranch, MOZ_KnownLive(mTeeState), aRv); + + // Step 6: Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined( + mTeeState->GetStream()->GetParentObject(), aRv); + } + + // Step 19, 20 + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override { + // Step 1. + mTeeState->SetCanceled(mBranch, true); + + // Step 2. + mTeeState->SetReason(mBranch, aReason.Value()); + + // Step 3. + if (mTeeState->Canceled(otherStream())) { + // Step 3.1 + JS::Rooted<JSObject*> compositeReason(aCx, JS::NewArrayObject(aCx, 2)); + if (!compositeReason) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + JS::Rooted<JS::Value> reason1(aCx, mTeeState->Reason1()); + if (!JS_SetElement(aCx, compositeReason, 0, reason1)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + JS::Rooted<JS::Value> reason2(aCx, mTeeState->Reason2()); + if (!JS_SetElement(aCx, compositeReason, 1, reason2)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 3.2 + JS::Rooted<JS::Value> compositeReasonValue( + aCx, JS::ObjectValue(*compositeReason)); + RefPtr<ReadableStream> stream(mTeeState->GetStream()); + RefPtr<Promise> cancelResult = + ReadableStreamCancel(aCx, stream, compositeReasonValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 3.3 + mTeeState->CancelPromise()->MaybeResolve(cancelResult); + } + + // Step 4. + return do_AddRef(mTeeState->CancelPromise()); + }; + + protected: + ~ByteStreamTeeSourceAlgorithms() override = default; + + private: + TeeBranch otherStream() { return OtherTeeBranch(mBranch); } + + RefPtr<TeeState> mTeeState; + TeeBranch mBranch; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ByteStreamTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase, mTeeState) +NS_IMPL_ADDREF_INHERITED(ByteStreamTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(ByteStreamTeeSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ByteStreamTeeSourceAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsBase) + +struct PullWithDefaultReaderReadRequest final : public ReadRequest { + RefPtr<TeeState> mTeeState; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PullWithDefaultReaderReadRequest, + ReadRequest) + + explicit PullWithDefaultReaderReadRequest(TeeState* aTeeState) + : mTeeState(aTeeState) {} + + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + // https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee + // Step 15.2.1 + class PullWithDefaultReaderChunkStepMicrotask : public MicroTaskRunnable { + RefPtr<TeeState> mTeeState; + JS::PersistentRooted<JSObject*> mChunk; + + public: + PullWithDefaultReaderChunkStepMicrotask(JSContext* aCx, + TeeState* aTeeState, + JS::Handle<JSObject*> aChunk) + : mTeeState(aTeeState), mChunk(aCx, aChunk) {} + + MOZ_CAN_RUN_SCRIPT + void Run(AutoSlowOperation& aAso) override { + // Step Numbering in this function is relative to the Queue a microtask + // of the Chunk steps of 15.2.1 of + // https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee + AutoJSAPI jsapi; + if (NS_WARN_IF( + !jsapi.Init(mTeeState->GetStream()->GetParentObject()))) { + return; + } + JSContext* cx = jsapi.cx(); + + // Step 1. Set readAgainForBranch1 to false. + mTeeState->SetReadAgainForBranch1(false); + + // Step 2. Set readAgainForBranch2 to false. + mTeeState->SetReadAgainForBranch2(false); + + // Step 3. Let chunk1 and chunk2 be chunk. + JS::Rooted<JSObject*> chunk1(cx, mChunk); + JS::Rooted<JSObject*> chunk2(cx, mChunk); + + // Step 4. If canceled1 is false and canceled2 is false, + ErrorResult rv; + if (!mTeeState->Canceled1() && !mTeeState->Canceled2()) { + // Step 4.1. Let cloneResult be CloneAsUint8Array(chunk). + JS::Rooted<JSObject*> cloneResult(cx, CloneAsUint8Array(cx, mChunk)); + + // Step 4.2. If cloneResult is an abrupt completion, + if (!cloneResult) { + // Step 4.2.1 Perform + // !ReadableByteStreamControllerError(branch1.[[controller]], + // cloneResult.[[Value]]). + JS::Rooted<JS::Value> exceptionValue(cx); + if (!JS_GetPendingException(cx, &exceptionValue)) { + // Uncatchable exception, simply return. + return; + } + JS_ClearPendingException(cx); + + ErrorResult rv; + ReadableByteStreamControllerError( + mTeeState->Branch1()->Controller()->AsByte(), exceptionValue, + rv); + if (rv.MaybeSetPendingException( + cx, "Error during ReadableByteStreamControllerError")) { + return; + } + + // Step 4.2.2. Perform ! + // ReadableByteStreamControllerError(branch2.[[controller]], + // cloneResult.[[Value]]). + ReadableByteStreamControllerError( + mTeeState->Branch2()->Controller()->AsByte(), exceptionValue, + rv); + if (rv.MaybeSetPendingException( + cx, "Error during ReadableByteStreamControllerError")) { + return; + } + + // Step 4.2.3. Resolve cancelPromise with ! + // ReadableStreamCancel(stream, cloneResult.[[Value]]). + RefPtr<ReadableStream> stream(mTeeState->GetStream()); + RefPtr<Promise> promise = + ReadableStreamCancel(cx, stream, exceptionValue, rv); + if (rv.MaybeSetPendingException( + cx, "Error during ReadableByteStreamControllerError")) { + return; + } + mTeeState->CancelPromise()->MaybeResolve(promise); + + // Step 4.2.4. Return. + return; + } + + // Step 4.3. Otherwise, set chunk2 to cloneResult.[[Value]]. + chunk2 = cloneResult; + } + + // Step 5. If canceled1 is false, + // perform ! ReadableByteStreamControllerEnqueue(branch1.[[controller]], + // chunk1). + if (!mTeeState->Canceled1()) { + ErrorResult rv; + RefPtr<ReadableByteStreamController> controller( + mTeeState->Branch1()->Controller()->AsByte()); + ReadableByteStreamControllerEnqueue(cx, controller, chunk1, rv); + if (rv.MaybeSetPendingException( + cx, "Error during ReadableByteStreamControllerEnqueue")) { + return; + } + } + + // Step 6. If canceled2 is false, + // perform ! ReadableByteStreamControllerEnqueue(branch2.[[controller]], + // chunk2). + if (!mTeeState->Canceled2()) { + ErrorResult rv; + RefPtr<ReadableByteStreamController> controller( + mTeeState->Branch2()->Controller()->AsByte()); + ReadableByteStreamControllerEnqueue(cx, controller, chunk2, rv); + if (rv.MaybeSetPendingException( + cx, "Error during ReadableByteStreamControllerEnqueue")) { + return; + } + } + + // Step 7. Set reading to false. + mTeeState->SetReading(false); + + // Step 8. If readAgainForBranch1 is true, perform pull1Algorithm. + if (mTeeState->ReadAgainForBranch1()) { + ByteStreamTeePullAlgorithm(cx, TeeBranch::Branch1, + MOZ_KnownLive(mTeeState), rv); + } else if (mTeeState->ReadAgainForBranch2()) { + // Step 9. Otherwise, if readAgainForBranch2 is true, perform + // pull2Algorithm. + ByteStreamTeePullAlgorithm(cx, TeeBranch::Branch2, + MOZ_KnownLive(mTeeState), rv); + } + } + + bool Suppressed() override { + nsIGlobalObject* global = mTeeState->GetStream()->GetParentObject(); + return global && global->IsInSyncOperation(); + } + }; + + MOZ_ASSERT(aChunk.isObjectOrNull()); + MOZ_ASSERT(aChunk.toObjectOrNull() != nullptr); + JS::Rooted<JSObject*> chunk(aCx, &aChunk.toObject()); + RefPtr<PullWithDefaultReaderChunkStepMicrotask> task = + MakeRefPtr<PullWithDefaultReaderChunkStepMicrotask>(aCx, mTeeState, + chunk); + CycleCollectedJSContext::Get()->DispatchToMicroTask(task.forget()); + } + + MOZ_CAN_RUN_SCRIPT void CloseSteps(JSContext* aCx, + ErrorResult& aRv) override { + // Step numbering below is relative to Step 15.2. 'close steps' of + // https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee + + // Step 1. Set reading to false. + mTeeState->SetReading(false); + + // Step 2. If canceled1 is false, perform ! + // ReadableByteStreamControllerClose(branch1.[[controller]]). + RefPtr<ReadableByteStreamController> branch1Controller = + mTeeState->Branch1()->Controller()->AsByte(); + if (!mTeeState->Canceled1()) { + ReadableByteStreamControllerClose(aCx, branch1Controller, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 3. If canceled2 is false, perform ! + // ReadableByteStreamControllerClose(branch2.[[controller]]). + RefPtr<ReadableByteStreamController> branch2Controller = + mTeeState->Branch2()->Controller()->AsByte(); + if (!mTeeState->Canceled2()) { + ReadableByteStreamControllerClose(aCx, branch2Controller, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 4. If branch1.[[controller]].[[pendingPullIntos]] is not empty, + // perform ! ReadableByteStreamControllerRespond(branch1.[[controller]], 0). + if (!branch1Controller->PendingPullIntos().isEmpty()) { + ReadableByteStreamControllerRespond(aCx, branch1Controller, 0, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 5. If branch2.[[controller]].[[pendingPullIntos]] is not empty, + // perform ! ReadableByteStreamControllerRespond(branch2.[[controller]], 0). + if (!branch2Controller->PendingPullIntos().isEmpty()) { + ReadableByteStreamControllerRespond(aCx, branch2Controller, 0, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 6. If canceled1 is false or canceled2 is false, resolve + // cancelPromise with undefined. + if (!mTeeState->Canceled1() || !mTeeState->Canceled2()) { + mTeeState->CancelPromise()->MaybeResolveWithUndefined(); + } + } + + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) override { + mTeeState->SetReading(false); + } + + protected: + ~PullWithDefaultReaderReadRequest() override = default; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PullWithDefaultReaderReadRequest, + ReadRequest, mTeeState) +NS_IMPL_ADDREF_INHERITED(PullWithDefaultReaderReadRequest, ReadRequest) +NS_IMPL_RELEASE_INHERITED(PullWithDefaultReaderReadRequest, ReadRequest) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PullWithDefaultReaderReadRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadRequest) + +void ForwardReaderError(TeeState* aTeeState, + ReadableStreamGenericReader* aThisReader); + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee: +// Step 15. +void PullWithDefaultReader(JSContext* aCx, TeeState* aTeeState, + ErrorResult& aRv) { + RefPtr<ReadableStreamGenericReader> reader = aTeeState->GetReader(); + + // Step 15.1. If reader implements ReadableStreamBYOBReader, + if (reader->IsBYOB()) { + // Step 15.1.1. Assert: reader.[[readIntoRequests]] is empty. + MOZ_ASSERT(reader->AsBYOB()->ReadIntoRequests().length() == 0); + + // Step 15.1.2. Perform ! ReadableStreamBYOBReaderRelease(reader). + ReadableStreamBYOBReaderRelease(aCx, reader->AsBYOB(), aRv); + if (aRv.Failed()) { + return; + } + + // Step 15.1.3. Set reader to ! AcquireReadableStreamDefaultReader(stream). + reader = AcquireReadableStreamDefaultReader(aTeeState->GetStream(), aRv); + if (aRv.Failed()) { + return; + } + aTeeState->SetReader(reader); + + // Step 16.1.4. Perform forwardReaderError, given reader. + ForwardReaderError(aTeeState, reader); + } + + // Step 15.2 + RefPtr<ReadRequest> readRequest = + new PullWithDefaultReaderReadRequest(aTeeState); + + // Step 15.3 + ReadableStreamDefaultReaderRead(aCx, reader, readRequest, aRv); +} + +class PullWithBYOBReader_ReadIntoRequest final : public ReadIntoRequest { + RefPtr<TeeState> mTeeState; + const TeeBranch mForBranch; + ~PullWithBYOBReader_ReadIntoRequest() override = default; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PullWithBYOBReader_ReadIntoRequest, + ReadIntoRequest) + + explicit PullWithBYOBReader_ReadIntoRequest(TeeState* aTeeState, + TeeBranch aForBranch) + : mTeeState(aTeeState), mForBranch(aForBranch) {} + + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + // https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee + // Step 16.4 chunk steps, Step 1. + class PullWithBYOBReaderChunkMicrotask : public MicroTaskRunnable { + RefPtr<TeeState> mTeeState; + JS::PersistentRooted<JSObject*> mChunk; + const TeeBranch mForBranch; + + public: + PullWithBYOBReaderChunkMicrotask(JSContext* aCx, TeeState* aTeeState, + JS::Handle<JSObject*> aChunk, + TeeBranch aForBranch) + : mTeeState(aTeeState), mChunk(aCx, aChunk), mForBranch(aForBranch) {} + + MOZ_CAN_RUN_SCRIPT + void Run(AutoSlowOperation& aAso) override { + AutoJSAPI jsapi; + if (NS_WARN_IF( + !jsapi.Init(mTeeState->GetStream()->GetParentObject()))) { + return; + } + JSContext* cx = jsapi.cx(); + ErrorResult rv; + // https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee + // + // Step Numbering below is relative to Chunk steps Microtask at + // Step 16.4 chunk steps, Step 1. + + // Step 1. + mTeeState->SetReadAgainForBranch1(false); + + // Step 2. + mTeeState->SetReadAgainForBranch2(false); + + // Step 3. + bool byobCanceled = mTeeState->Canceled(mForBranch); + // Step 4. + bool otherCanceled = mTeeState->Canceled(OtherTeeBranch(mForBranch)); + + // Rather than store byobBranch / otherBranch, we re-derive the pointers + // below, as borrowed from steps 16.2/16.3 + ReadableStream* byobBranch = mTeeState->Branch(mForBranch); + ReadableStream* otherBranch = + mTeeState->Branch(OtherTeeBranch(mForBranch)); + + // Step 5. + if (!otherCanceled) { + // Step 5.1 (using the name clonedChunk because we don't want to name + // the completion record explicitly) + JS::Rooted<JSObject*> clonedChunk(cx, CloneAsUint8Array(cx, mChunk)); + + // Step 5.2. If cloneResult is an abrupt completion, + if (!clonedChunk) { + JS::Rooted<JS::Value> exception(cx); + if (!JS_GetPendingException(cx, &exception)) { + // Uncatchable exception. Return with pending + // exception still on context. + return; + } + + // It's not expliclitly stated, but I assume the intention here is + // that we perform a normal completion here, so we clear the + // exception. + JS_ClearPendingException(cx); + + // Step 5.2.1 + + ReadableByteStreamControllerError( + byobBranch->Controller()->AsByte(), exception, rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + + // Step 5.2.2. + ReadableByteStreamControllerError( + otherBranch->Controller()->AsByte(), exception, rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + + // Step 5.2.3. + RefPtr<ReadableStream> stream = mTeeState->GetStream(); + RefPtr<Promise> cancelPromise = + ReadableStreamCancel(cx, stream, exception, rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + mTeeState->CancelPromise()->MaybeResolve(cancelPromise); + + // Step 5.2.4. + return; + } + + // Step 5.3 (implicitly handled above by name selection) + // Step 5.4. + if (!byobCanceled) { + RefPtr<ReadableByteStreamController> controller( + byobBranch->Controller()->AsByte()); + ReadableByteStreamControllerRespondWithNewView(cx, controller, + mChunk, rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + } + + // Step 5.4. + RefPtr<ReadableByteStreamController> otherController = + otherBranch->Controller()->AsByte(); + ReadableByteStreamControllerEnqueue(cx, otherController, clonedChunk, + rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + // Step 6. + } else if (!byobCanceled) { + RefPtr<ReadableByteStreamController> byobController = + byobBranch->Controller()->AsByte(); + ReadableByteStreamControllerRespondWithNewView(cx, byobController, + mChunk, rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + } + + // Step 7. + mTeeState->SetReading(false); + + // Step 8. + if (mTeeState->ReadAgainForBranch1()) { + ByteStreamTeePullAlgorithm(cx, TeeBranch::Branch1, + MOZ_KnownLive(mTeeState), rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + } else if (mTeeState->ReadAgainForBranch2()) { + ByteStreamTeePullAlgorithm(cx, TeeBranch::Branch2, + MOZ_KnownLive(mTeeState), rv); + if (rv.MaybeSetPendingException(cx)) { + return; + } + } + } + + bool Suppressed() override { + nsIGlobalObject* global = mTeeState->GetStream()->GetParentObject(); + return global && global->IsInSyncOperation(); + } + }; + + MOZ_ASSERT(aChunk.isObjectOrNull()); + MOZ_ASSERT(aChunk.toObjectOrNull()); + JS::Rooted<JSObject*> chunk(aCx, aChunk.toObjectOrNull()); + RefPtr<PullWithBYOBReaderChunkMicrotask> task = + MakeRefPtr<PullWithBYOBReaderChunkMicrotask>(aCx, mTeeState, chunk, + mForBranch); + CycleCollectedJSContext::Get()->DispatchToMicroTask(task.forget()); + } + + MOZ_CAN_RUN_SCRIPT + void CloseSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + // Step 1. + mTeeState->SetReading(false); + + // Step 2. + bool byobCanceled = mTeeState->Canceled(mForBranch); + + // Step 3. + bool otherCanceled = mTeeState->Canceled(OtherTeeBranch(mForBranch)); + + // Rather than store byobBranch / otherBranch, we re-derive the pointers + // below, as borrowed from steps 16.2/16.3 + ReadableStream* byobBranch = mTeeState->Branch(mForBranch); + ReadableStream* otherBranch = mTeeState->Branch(OtherTeeBranch(mForBranch)); + + // Step 4. + if (!byobCanceled) { + RefPtr<ReadableByteStreamController> controller = + byobBranch->Controller()->AsByte(); + ReadableByteStreamControllerClose(aCx, controller, aRv); + if (aRv.Failed()) { + return; + } + } + // Step 5. + if (!otherCanceled) { + RefPtr<ReadableByteStreamController> controller = + otherBranch->Controller()->AsByte(); + ReadableByteStreamControllerClose(aCx, controller, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 6. + if (!aChunk.isUndefined()) { + MOZ_ASSERT(aChunk.isObject()); + MOZ_ASSERT(aChunk.toObjectOrNull()); + + JS::Rooted<JSObject*> chunkObject(aCx, &aChunk.toObject()); + MOZ_ASSERT(JS_IsArrayBufferViewObject(chunkObject)); + // Step 6.1. + MOZ_ASSERT(JS_GetArrayBufferViewByteLength(chunkObject) == 0); + + // Step 6.2. + if (!byobCanceled) { + RefPtr<ReadableByteStreamController> byobController( + byobBranch->Controller()->AsByte()); + ReadableByteStreamControllerRespondWithNewView(aCx, byobController, + chunkObject, aRv); + if (aRv.Failed()) { + return; + } + } + + // Step 6.3 + if (!otherCanceled && + !otherBranch->Controller()->AsByte()->PendingPullIntos().isEmpty()) { + RefPtr<ReadableByteStreamController> otherController( + otherBranch->Controller()->AsByte()); + ReadableByteStreamControllerRespond(aCx, otherController, 0, aRv); + if (aRv.Failed()) { + return; + } + } + } + + // Step 7. + if (!byobCanceled || !otherCanceled) { + mTeeState->CancelPromise()->MaybeResolveWithUndefined(); + } + } + + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> e, + ErrorResult& aRv) override { + // Step 1. + mTeeState->SetReading(false); + } +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PullWithBYOBReader_ReadIntoRequest, + ReadIntoRequest, mTeeState) +NS_IMPL_ADDREF_INHERITED(PullWithBYOBReader_ReadIntoRequest, ReadIntoRequest) +NS_IMPL_RELEASE_INHERITED(PullWithBYOBReader_ReadIntoRequest, ReadIntoRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PullWithBYOBReader_ReadIntoRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadIntoRequest) + +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee +// Step 16. +void PullWithBYOBReader(JSContext* aCx, TeeState* aTeeState, + JS::Handle<JSObject*> aView, TeeBranch aForBranch, + ErrorResult& aRv) { + // Step 16.1 + if (aTeeState->GetReader()->IsDefault()) { + // Step 16.1.1 + MOZ_ASSERT(aTeeState->GetDefaultReader()->ReadRequests().isEmpty()); + + // Step 16.1.2. Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReaderRelease(aCx, aTeeState->GetDefaultReader(), aRv); + if (aRv.Failed()) { + return; + } + + // Step 16.1.3. Set reader to !AcquireReadableStreamBYOBReader(stream). + RefPtr<ReadableStreamBYOBReader> reader = + AcquireReadableStreamBYOBReader(aTeeState->GetStream(), aRv); + if (aRv.Failed()) { + return; + } + aTeeState->SetReader(reader); + + // Step 16.1.4. Perform forwardReaderError, given reader. + ForwardReaderError(aTeeState, reader); + } + + // Step 16.2. Unused in this function, moved to consumers. + // Step 16.3. Unused in this function, moved to consumers. + + // Step 16.4. + RefPtr<ReadIntoRequest> readIntoRequest = + new PullWithBYOBReader_ReadIntoRequest(aTeeState, aForBranch); + + // Step 16.5. + RefPtr<ReadableStreamBYOBReader> byobReader = + aTeeState->GetReader()->AsBYOB(); + ReadableStreamBYOBReaderRead(aCx, byobReader, aView, readIntoRequest, aRv); +} + +// See https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee +// Step 14. +void ForwardReaderError(TeeState* aTeeState, + ReadableStreamGenericReader* aThisReader) { + aThisReader->ClosedPromise()->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + TeeState* aTeeState, ReadableStreamGenericReader* aThisReader) {}, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + TeeState* aTeeState, ReadableStreamGenericReader* aReader) { + // Step 14.1.1 + if (aTeeState->GetReader() != aReader) { + return; + } + + ErrorResult rv; + // Step 14.1.2: Perform + // !ReadableByteStreamControllerError(branch1.[[controller]], r). + MOZ_ASSERT(aTeeState->Branch1()->Controller()->IsByte()); + ReadableByteStreamControllerError( + aTeeState->Branch1()->Controller()->AsByte(), aValue, aRv); + if (aRv.Failed()) { + return; + } + + // Step 14.1.3: Perform + // !ReadableByteStreamControllerError(branch2.[[controller]], r). + MOZ_ASSERT(aTeeState->Branch2()->Controller()->IsByte()); + ReadableByteStreamControllerError( + aTeeState->Branch2()->Controller()->AsByte(), aValue, aRv); + if (aRv.Failed()) { + return; + } + + // Step 14.1.4: If canceled1 is false or canceled2 is false, resolve + // cancelPromise with undefined. + if (!aTeeState->Canceled1() || !aTeeState->Canceled2()) { + aTeeState->CancelPromise()->MaybeResolveWithUndefined(); + } + }, + RefPtr(aTeeState), RefPtr(aThisReader)); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee +void ReadableByteStreamTee(JSContext* aCx, ReadableStream* aStream, + nsTArray<RefPtr<ReadableStream>>& aResult, + ErrorResult& aRv) { + // Step 1. Implicit + // Step 2. + MOZ_ASSERT(aStream->Controller()->IsByte()); + + // Step 3-13 captured as part of TeeState allocation + RefPtr<TeeState> teeState = TeeState::Create(aStream, false, aRv); + if (aRv.Failed()) { + return; + } + + // Step 14: See ForwardReaderError + // Step 15. See PullWithDefaultReader + // Step 16. See PullWithBYOBReader + // Step 17,18. See {Native,}ByteStreamTeePullAlgorithm + // Step 19,20. See ReadableByteStreamTeeCancelAlgorithm + // Step 21. Elided because consumers know how to handle nullptr correctly. + // Step 22. + nsCOMPtr<nsIGlobalObject> global = aStream->GetParentObject(); + auto branch1Algorithms = + MakeRefPtr<ByteStreamTeeSourceAlgorithms>(teeState, TeeBranch::Branch1); + teeState->SetBranch1( + ReadableStream::CreateByteAbstract(aCx, global, branch1Algorithms, aRv)); + if (aRv.Failed()) { + return; + } + + // Step 23. + auto branch2Algorithms = + MakeRefPtr<ByteStreamTeeSourceAlgorithms>(teeState, TeeBranch::Branch2); + teeState->SetBranch2( + ReadableStream::CreateByteAbstract(aCx, global, branch2Algorithms, aRv)); + if (aRv.Failed()) { + return; + } + + // Step 24. + ForwardReaderError(teeState, teeState->GetReader()); + + // Step 25. + aResult.AppendElement(teeState->Branch1()); + aResult.AppendElement(teeState->Branch2()); +} +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/ReadableStreamTee.h b/dom/streams/ReadableStreamTee.h new file mode 100644 index 0000000000..855fcf4286 --- /dev/null +++ b/dom/streams/ReadableStreamTee.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ReadableStreamTee_h +#define mozilla_dom_ReadableStreamTee_h + +#include "mozilla/dom/ReadRequest.h" +#include "mozilla/dom/ReadableStreamController.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { +struct TeeState; +enum class TeeBranch : bool; +class ReadableStream; + +// Implementation of the Pull algorithm steps for ReadableStreamDefaultTee, +// from +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee +// Step 12. +class ReadableStreamDefaultTeeSourceAlgorithms final + : public UnderlyingSourceAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + ReadableStreamDefaultTeeSourceAlgorithms, UnderlyingSourceAlgorithmsBase) + + ReadableStreamDefaultTeeSourceAlgorithms(TeeState* aTeeState, + TeeBranch aBranch) + : mTeeState(aTeeState), mBranch(aBranch) {} + + MOZ_CAN_RUN_SCRIPT void StartCallback(JSContext* aCx, + ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override { + aRetVal.setUndefined(); + } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PullCallback( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override; + + protected: + ~ReadableStreamDefaultTeeSourceAlgorithms() override = default; + + private: + // Virtually const, but is cycle collected + MOZ_KNOWN_LIVE RefPtr<TeeState> mTeeState; + TeeBranch mBranch; +}; + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee +// Step 12.3 +struct ReadableStreamDefaultTeeReadRequest final : public ReadRequest { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ReadableStreamDefaultTeeReadRequest, + ReadRequest) + + RefPtr<TeeState> mTeeState; + + explicit ReadableStreamDefaultTeeReadRequest(TeeState* aTeeState) + : mTeeState(aTeeState) {} + + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT void CloseSteps(JSContext* aCx, ErrorResult& aRv) override; + + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) override; + + protected: + ~ReadableStreamDefaultTeeReadRequest() override = default; +}; + +namespace streams_abstract { +MOZ_CAN_RUN_SCRIPT void ReadableByteStreamTee( + JSContext* aCx, ReadableStream* aStream, + nsTArray<RefPtr<ReadableStream>>& aResult, ErrorResult& aRv); +} + +} // namespace mozilla::dom +#endif diff --git a/dom/streams/StreamUtils.cpp b/dom/streams/StreamUtils.cpp new file mode 100644 index 0000000000..15ad80867d --- /dev/null +++ b/dom/streams/StreamUtils.cpp @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StreamUtils.h" + +#include <cmath> +#include "mozilla/dom/QueuingStrategyBinding.h" + +namespace mozilla::dom { + +// Streams Spec: 7.4 +// https://streams.spec.whatwg.org/#validate-and-normalize-high-water-mark +double ExtractHighWaterMark(const QueuingStrategy& aStrategy, + double aDefaultHWM, mozilla::ErrorResult& aRv) { + // Step 1. + if (!aStrategy.mHighWaterMark.WasPassed()) { + return aDefaultHWM; + } + + // Step 2. + double highWaterMark = aStrategy.mHighWaterMark.Value(); + + // Step 3. + if (std::isnan(highWaterMark) || highWaterMark < 0) { + aRv.ThrowRangeError("Invalid highWaterMark"); + return 0.0; + } + + // Step 4. + return highWaterMark; +} + +} // namespace mozilla::dom diff --git a/dom/streams/StreamUtils.h b/dom/streams/StreamUtils.h new file mode 100644 index 0000000000..f0b09c63b8 --- /dev/null +++ b/dom/streams/StreamUtils.h @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_StreamUtils_h +#define mozilla_dom_StreamUtils_h + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Promise.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +struct QueuingStrategy; + +double ExtractHighWaterMark(const QueuingStrategy& aStrategy, + double aDefaultHWM, ErrorResult& aRv); + +// Promisification algorithm, shared among: +// Step 2 and 3 of https://streams.spec.whatwg.org/#readablestream-set-up +// Step 2 and 3 of +// https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support +// Step 2 and 3 of https://streams.spec.whatwg.org/#writablestream-set-up +// Step 5 and 6 of https://streams.spec.whatwg.org/#transformstream-set-up +template <typename T> +MOZ_CAN_RUN_SCRIPT static already_AddRefed<Promise> PromisifyAlgorithm( + nsIGlobalObject* aGlobal, T aFunc, mozilla::ErrorResult& aRv) { + // Step 1. Let result be the result of running (algorithm). If this throws an + // exception e, return a promise rejected with e. + RefPtr<Promise> result; + if constexpr (!std::is_same<decltype(aFunc(aRv)), void>::value) { + result = aFunc(aRv); + } else { + aFunc(aRv); + } + + if (aRv.IsUncatchableException()) { + return nullptr; + } + + if (aRv.Failed()) { + return Promise::CreateRejectedWithErrorResult(aGlobal, aRv); + } + + // Step 2. If result is a Promise, then return result. + if (result) { + return result.forget(); + } + + // Step 3. Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(aGlobal, aRv); +} + +} // namespace mozilla::dom + +#endif // mozilla_dom_StreamUtils_h diff --git a/dom/streams/TeeState.cpp b/dom/streams/TeeState.cpp new file mode 100644 index 0000000000..8600ea1205 --- /dev/null +++ b/dom/streams/TeeState.cpp @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TeeState.h" + +#include "ReadableStreamTee.h" +#include "js/Value.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/Promise.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(TeeState, + (mStream, mReader, mBranch1, mBranch2, + mCancelPromise), + (mReason1, mReason2)) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TeeState) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TeeState) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TeeState) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TeeState::TeeState(ReadableStream* aStream, bool aCloneForBranch2) + : mStream(aStream), + mReason1(JS::NullValue()), + mReason2(JS::NullValue()), + mCloneForBranch2(aCloneForBranch2) { + mozilla::HoldJSObjects(this); + MOZ_RELEASE_ASSERT(!aCloneForBranch2, + "cloneForBranch2 path is not implemented."); +} + +already_AddRefed<TeeState> TeeState::Create(ReadableStream* aStream, + bool aCloneForBranch2, + ErrorResult& aRv) { + RefPtr<TeeState> teeState = new TeeState(aStream, aCloneForBranch2); + + RefPtr<ReadableStreamDefaultReader> reader = + AcquireReadableStreamDefaultReader(teeState->GetStream(), aRv); + if (aRv.Failed()) { + return nullptr; + } + teeState->SetReader(reader); + + RefPtr<Promise> promise = + Promise::CreateInfallible(teeState->GetStream()->GetParentObject()); + teeState->SetCancelPromise(promise); + + return teeState.forget(); +} + +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee +// Pull Algorithm Steps: +void TeeState::PullCallback(JSContext* aCx, nsIGlobalObject* aGlobal, + ErrorResult& aRv) { + // Step 13.1: If reading is true, + if (Reading()) { + // Step 13.1.1: Set readAgain to true. + SetReadAgain(true); + + // Step 13.1.2: Return a promise resolved with undefined. + // (The caller will create it if necessary) + return; + } + + // Step 13.2: Set reading to true. + SetReading(true); + + // Step 13.3: Let readRequest be a read request with the following items: + RefPtr<ReadRequest> readRequest = + new ReadableStreamDefaultTeeReadRequest(this); + + // Step 13.4: Perform ! ReadableStreamDefaultReaderRead(reader, readRequest). + RefPtr<ReadableStreamGenericReader> reader = GetReader(); + ReadableStreamDefaultReaderRead(aCx, reader, readRequest, aRv); + + // Step 13.5: Return a promise resolved with undefined. + // (The caller will create it if necessary) +} + +} // namespace mozilla::dom diff --git a/dom/streams/TeeState.h b/dom/streams/TeeState.h new file mode 100644 index 0000000000..9a688be662 --- /dev/null +++ b/dom/streams/TeeState.h @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TeeState_h +#define mozilla_dom_TeeState_h + +#include "mozilla/HoldDropJSObjects.h" +#include "nsISupports.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/Promise.h" + +namespace mozilla::dom { + +class ReadableStreamDefaultTeeSourceAlgorithms; + +enum class TeeBranch : bool { + Branch1, + Branch2, +}; + +inline TeeBranch OtherTeeBranch(TeeBranch aBranch) { + if (aBranch == TeeBranch::Branch1) { + return TeeBranch::Branch2; + } + return TeeBranch::Branch1; +} + +// A closure capturing the free variables in the ReadableStreamTee family of +// algorithms. +// https://streams.spec.whatwg.org/#abstract-opdef-readablestreamdefaulttee +// https://streams.spec.whatwg.org/#abstract-opdef-readablebytestreamtee +struct TeeState : public nsISupports { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(TeeState) + + static already_AddRefed<TeeState> Create(ReadableStream* aStream, + bool aCloneForBranch2, + ErrorResult& aRv); + + ReadableStream* GetStream() const { return mStream; } + void SetStream(ReadableStream* aStream) { mStream = aStream; } + + ReadableStreamGenericReader* GetReader() const { return mReader; } + void SetReader(ReadableStreamGenericReader* aReader) { mReader = aReader; } + + ReadableStreamDefaultReader* GetDefaultReader() const { + return mReader->AsDefault(); + } + + bool ReadAgain() const { return mReadAgain; } + void SetReadAgain(bool aReadAgain) { mReadAgain = aReadAgain; } + + // ReadableByteStreamTee uses ReadAgainForBranch{1,2}; + bool ReadAgainForBranch1() const { return ReadAgain(); } + void SetReadAgainForBranch1(bool aReadAgainForBranch1) { + SetReadAgain(aReadAgainForBranch1); + } + + bool ReadAgainForBranch2() const { return mReadAgainForBranch2; } + void SetReadAgainForBranch2(bool aReadAgainForBranch2) { + mReadAgainForBranch2 = aReadAgainForBranch2; + } + + bool Reading() const { return mReading; } + void SetReading(bool aReading) { mReading = aReading; } + + bool Canceled1() const { return mCanceled1; } + void SetCanceled1(bool aCanceled1) { mCanceled1 = aCanceled1; } + + bool Canceled2() const { return mCanceled2; } + void SetCanceled2(bool aCanceled2) { mCanceled2 = aCanceled2; } + + void SetCanceled(TeeBranch aBranch, bool aCanceled) { + aBranch == TeeBranch::Branch1 ? SetCanceled1(aCanceled) + : SetCanceled2(aCanceled); + } + bool Canceled(TeeBranch aBranch) { + return aBranch == TeeBranch::Branch1 ? Canceled1() : Canceled2(); + } + + JS::Value Reason1() const { return mReason1; } + void SetReason1(JS::Handle<JS::Value> aReason1) { mReason1 = aReason1; } + + JS::Value Reason2() const { return mReason2; } + void SetReason2(JS::Handle<JS::Value> aReason2) { mReason2 = aReason2; } + + void SetReason(TeeBranch aBranch, JS::Handle<JS::Value> aReason) { + aBranch == TeeBranch::Branch1 ? SetReason1(aReason) : SetReason2(aReason); + } + + ReadableStream* Branch1() const { return mBranch1; } + void SetBranch1(already_AddRefed<ReadableStream> aBranch1) { + mBranch1 = aBranch1; + } + + ReadableStream* Branch2() const { return mBranch2; } + void SetBranch2(already_AddRefed<ReadableStream> aBranch2) { + mBranch2 = aBranch2; + } + + Promise* CancelPromise() const { return mCancelPromise; } + void SetCancelPromise(Promise* aCancelPromise) { + mCancelPromise = aCancelPromise; + } + + bool CloneForBranch2() const { return mCloneForBranch2; } + void setCloneForBranch2(bool aCloneForBranch2) { + mCloneForBranch2 = aCloneForBranch2; + } + + // Some code is better served by using an enum into various internal slots to + // avoid duplication: Here we provide alternative accessors for that case. + ReadableStream* Branch(TeeBranch aBranch) const { + return aBranch == TeeBranch::Branch1 ? Branch1() : Branch2(); + } + + void SetReadAgainForBranch(TeeBranch aBranch, bool aValue) { + if (aBranch == TeeBranch::Branch1) { + SetReadAgainForBranch1(aValue); + return; + } + SetReadAgainForBranch2(aValue); + } + + MOZ_CAN_RUN_SCRIPT void PullCallback(JSContext* aCx, nsIGlobalObject* aGlobal, + ErrorResult& aRv); + + private: + TeeState(ReadableStream* aStream, bool aCloneForBranch2); + + // Implicit: + RefPtr<ReadableStream> mStream; + + // Step 3. (Step Numbering is based on ReadableStreamDefaultTee) + RefPtr<ReadableStreamGenericReader> mReader; + + // Step 4. + bool mReading = false; + + // Step 5. + // (Aliased to readAgainForBranch1, for the purpose of ReadableByteStreamTee) + bool mReadAgain = false; + + // ReadableByteStreamTee + bool mReadAgainForBranch2 = false; + + // Step 6. + bool mCanceled1 = false; + + // Step 7. + bool mCanceled2 = false; + + // Step 8. + JS::Heap<JS::Value> mReason1; + + // Step 9. + JS::Heap<JS::Value> mReason2; + + // Step 10. + RefPtr<ReadableStream> mBranch1; + + // Step 11. + RefPtr<ReadableStream> mBranch2; + + // Step 12. + RefPtr<Promise> mCancelPromise; + + // Implicit: + bool mCloneForBranch2 = false; + + virtual ~TeeState() { mozilla::DropJSObjects(this); } +}; + +} // namespace mozilla::dom +#endif diff --git a/dom/streams/Transferable.cpp b/dom/streams/Transferable.cpp new file mode 100644 index 0000000000..f3e9504d86 --- /dev/null +++ b/dom/streams/Transferable.cpp @@ -0,0 +1,1068 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ErrorList.h" +#include "ReadableStreamPipeTo.h" +#include "js/RootingAPI.h" +#include "js/String.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDOMEventListener.h" +#include "nsIGlobalObject.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/WritableStream.h" +#include "mozilla/dom/TransformStream.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +static void PackAndPostMessage(JSContext* aCx, MessagePort* aPort, + const nsAString& aType, + JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + JS::Rooted<JSObject*> obj(aCx, + JS_NewObjectWithGivenProto(aCx, nullptr, nullptr)); + if (!obj) { + // XXX: Should we crash here and there? See also bug 1762233. + JS_ClearPendingException(aCx); + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + JS::Rooted<JS::Value> type(aCx); + if (!xpc::NonVoidStringToJsval(aCx, aType, &type)) { + JS_ClearPendingException(aCx); + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + if (!JS_DefineProperty(aCx, obj, "type", type, JSPROP_ENUMERATE)) { + JS_ClearPendingException(aCx); + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + JS::Rooted<JS::Value> value(aCx, aValue); + if (!JS_WrapValue(aCx, &value)) { + JS_ClearPendingException(aCx); + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + if (!JS_DefineProperty(aCx, obj, "value", value, JSPROP_ENUMERATE)) { + JS_ClearPendingException(aCx); + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + Sequence<JSObject*> transferables; // none in this case + JS::Rooted<JS::Value> objValue(aCx, JS::ObjectValue(*obj)); + aPort->PostMessage(aCx, objValue, transferables, aRv); +} + +// https://streams.spec.whatwg.org/#abstract-opdef-crossrealmtransformsenderror +static void CrossRealmTransformSendError(JSContext* aCx, MessagePort* aPort, + JS::Handle<JS::Value> aError) { + // Step 1: Perform PackAndPostMessage(port, "error", error), discarding the + // result. + PackAndPostMessage(aCx, aPort, u"error"_ns, aError, IgnoreErrors()); +} + +class SetUpTransformWritableMessageEventListener final + : public nsIDOMEventListener { + public: + SetUpTransformWritableMessageEventListener( + WritableStreamDefaultController* aController, Promise* aPromise) + : mController(aController), mBackpressurePromise(aPromise) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(SetUpTransformWritableMessageEventListener) + + // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable + // The handler steps of Step 4. + MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override { + AutoJSAPI jsapi; + if (!jsapi.Init(mController->GetParentObject())) { + return NS_OK; + } + JSContext* cx = jsapi.cx(); + MessageEvent* messageEvent = aEvent->AsMessageEvent(); + if (NS_WARN_IF(!messageEvent || !messageEvent->IsTrusted())) { + return NS_OK; + } + + // Step 1: Let data be the data of the message. + JS::Rooted<JS::Value> dataValue(cx); + IgnoredErrorResult rv; + messageEvent->GetData(cx, &dataValue, rv); + if (rv.Failed()) { + return NS_OK; + } + + // Step 2: Assert: Type(data) is Object. + // (But we check in runtime instead to avoid potential malicious events from + // a compromised process. Same below.) + if (NS_WARN_IF(!dataValue.isObject())) { + return NS_OK; + } + JS::Rooted<JSObject*> data(cx, &dataValue.toObject()); + + // Step 3: Let type be ! Get(data, "type"). + JS::Rooted<JS::Value> type(cx); + if (!JS_GetProperty(cx, data, "type", &type)) { + // XXX: See bug 1762233 + JS_ClearPendingException(cx); + return NS_OK; + } + + // Step 4: Let value be ! Get(data, "value"). + JS::Rooted<JS::Value> value(cx); + if (!JS_GetProperty(cx, data, "value", &value)) { + JS_ClearPendingException(cx); + return NS_OK; + } + + // Step 5: Assert: Type(type) is String. + if (NS_WARN_IF(!type.isString())) { + return NS_OK; + } + + // Step 6: If type is "pull", + bool equals = false; + if (!JS_StringEqualsLiteral(cx, type.toString(), "pull", &equals)) { + JS_ClearPendingException(cx); + return NS_OK; + } + if (equals) { + // Step 6.1: If backpressurePromise is not undefined, + MaybeResolveAndClearBackpressurePromise(); + return NS_OK; // implicit + } + + // Step 7: If type is "error", + if (!JS_StringEqualsLiteral(cx, type.toString(), "error", &equals)) { + JS_ClearPendingException(cx); + return NS_OK; + } + if (equals) { + // Step 7.1: Perform ! + // WritableStreamDefaultControllerErrorIfNeeded(controller, value). + WritableStreamDefaultControllerErrorIfNeeded(cx, mController, value, rv); + if (rv.Failed()) { + return NS_OK; + } + + // Step 7.2: If backpressurePromise is not undefined, + MaybeResolveAndClearBackpressurePromise(); + return NS_OK; // implicit + } + + // Logically it should be unreachable here, but we should expect random + // malicious messages. + NS_WARNING("Got an unexpected type other than pull/error."); + return NS_OK; + } + + void MaybeResolveAndClearBackpressurePromise() { + if (mBackpressurePromise) { + mBackpressurePromise->MaybeResolveWithUndefined(); + mBackpressurePromise = nullptr; + } + } + + // Note: This promise field is shared with the sink algorithms. + Promise* BackpressurePromise() { return mBackpressurePromise; } + + void CreateBackpressurePromise() { + mBackpressurePromise = + Promise::CreateInfallible(mController->GetParentObject()); + } + + private: + ~SetUpTransformWritableMessageEventListener() = default; + + // mController never changes before CC + // TODO: MOZ_IMMUTABLE_OUTSIDE_CC + MOZ_KNOWN_LIVE RefPtr<WritableStreamDefaultController> mController; + RefPtr<Promise> mBackpressurePromise; +}; + +NS_IMPL_CYCLE_COLLECTION(SetUpTransformWritableMessageEventListener, + mController, mBackpressurePromise) +NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformWritableMessageEventListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SetUpTransformWritableMessageEventListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + SetUpTransformWritableMessageEventListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +class SetUpTransformWritableMessageErrorEventListener final + : public nsIDOMEventListener { + public: + SetUpTransformWritableMessageErrorEventListener( + WritableStreamDefaultController* aController, MessagePort* aPort) + : mController(aController), mPort(aPort) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS( + SetUpTransformWritableMessageErrorEventListener) + + // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable + // The handler steps of Step 5. + MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override { + auto cleanupPort = + MakeScopeExit([port = RefPtr<MessagePort>(mPort)]() { port->Close(); }); + + if (NS_WARN_IF(!aEvent->AsMessageEvent() || !aEvent->IsTrusted())) { + return NS_OK; + } + + // Step 1: Let error be a new "DataCloneError" DOMException. + RefPtr<DOMException> exception = + DOMException::Create(NS_ERROR_DOM_DATA_CLONE_ERR); + + AutoJSAPI jsapi; + if (!jsapi.Init(mPort->GetParentObject())) { + return NS_OK; + } + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> error(cx); + if (!ToJSValue(cx, *exception, &error)) { + return NS_OK; + } + + // Step 2: Perform ! CrossRealmTransformSendError(port, error). + CrossRealmTransformSendError(cx, mPort, error); + + // Step 3: Perform + // ! WritableStreamDefaultControllerErrorIfNeeded(controller, error). + WritableStreamDefaultControllerErrorIfNeeded(cx, mController, error, + IgnoreErrors()); + + // Step 4: Disentangle port. + // (Close() does it) + mPort->Close(); + cleanupPort.release(); + + return NS_OK; + } + + private: + ~SetUpTransformWritableMessageErrorEventListener() = default; + + // mController never changes before CC + // TODO: MOZ_IMMUTABLE_OUTSIDE_CC + MOZ_KNOWN_LIVE RefPtr<WritableStreamDefaultController> mController; + RefPtr<MessagePort> mPort; +}; + +NS_IMPL_CYCLE_COLLECTION(SetUpTransformWritableMessageErrorEventListener, + mController, mPort) +NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformWritableMessageErrorEventListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE( + SetUpTransformWritableMessageErrorEventListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + SetUpTransformWritableMessageErrorEventListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +// https://streams.spec.whatwg.org/#abstract-opdef-packandpostmessagehandlingerror +static bool PackAndPostMessageHandlingError( + JSContext* aCx, mozilla::dom::MessagePort* aPort, const nsAString& aType, + JS::Handle<JS::Value> aValue, JS::MutableHandle<JS::Value> aError) { + // Step 1: Let result be PackAndPostMessage(port, type, value). + ErrorResult rv; + PackAndPostMessage(aCx, aPort, aType, aValue, rv); + + // Step 2: If result is an abrupt completion, + if (rv.Failed()) { + // Step 2.2: Perform ! CrossRealmTransformSendError(port, result.[[Value]]). + MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), aError)); + CrossRealmTransformSendError(aCx, aPort, aError); + return false; + } + + // Step 3: Return result as a completion record. + return true; +} + +// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable +class CrossRealmWritableUnderlyingSinkAlgorithms final + : public UnderlyingSinkAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + CrossRealmWritableUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) + + CrossRealmWritableUnderlyingSinkAlgorithms( + SetUpTransformWritableMessageEventListener* aListener, MessagePort* aPort) + : mListener(aListener), mPort(aPort) {} + + void StartCallback(JSContext* aCx, + WritableStreamDefaultController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override { + // Step 7. Let startAlgorithm be an algorithm that returns undefined. + aRetVal.setUndefined(); + } + + already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) override { + // Step 1: If backpressurePromise is undefined, set backpressurePromise to a + // promise resolved with undefined. + // Note: This promise field is shared with the message event listener. + if (!mListener->BackpressurePromise()) { + mListener->CreateBackpressurePromise(); + mListener->BackpressurePromise()->MaybeResolveWithUndefined(); + } + + // Step 2: Return the result of reacting to backpressurePromise with the + // following fulfillment steps: + auto result = + mListener->BackpressurePromise()->ThenWithCycleCollectedArgsJS( + [](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv, + SetUpTransformWritableMessageEventListener* aListener, + MessagePort* aPort, + JS::Handle<JS::Value> aChunk) -> already_AddRefed<Promise> { + // Step 2.1: Set backpressurePromise to a new promise. + aListener->CreateBackpressurePromise(); + + // Step 2.2: Let result be PackAndPostMessageHandlingError(port, + // "chunk", chunk). + JS::Rooted<JS::Value> error(aCx); + bool result = PackAndPostMessageHandlingError( + aCx, aPort, u"chunk"_ns, aChunk, &error); + + // Step 2.3: If result is an abrupt completion, + if (!result) { + // Step 2.3.1: Disentangle port. + // (Close() does it) + aPort->Close(); + + // Step 2.3.2: Return a promise rejected with result.[[Value]]. + return Promise::CreateRejected(aPort->GetParentObject(), error, + aRv); + } + + // Step 2.4: Otherwise, return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined( + aPort->GetParentObject(), aRv); + }, + std::make_tuple(mListener, mPort), std::make_tuple(aChunk)); + if (result.isErr()) { + aRv.Throw(result.unwrapErr()); + return nullptr; + } + return result.unwrap().forget(); + } + + already_AddRefed<Promise> CloseCallback(JSContext* aCx, + ErrorResult& aRv) override { + // Step 1: Perform ! PackAndPostMessage(port, "close", undefined). + PackAndPostMessage(aCx, mPort, u"close"_ns, JS::UndefinedHandleValue, aRv); + // (We'll check the result after step 2) + + // Step 2: Disentangle port. + // (Close() will do this) + mPort->Close(); + + if (aRv.Failed()) { + return nullptr; + } + + // Step 3: Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv); + } + + already_AddRefed<Promise> AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override { + // Step 1: Let result be PackAndPostMessageHandlingError(port, "error", + // reason). + JS::Rooted<JS::Value> error(aCx); + bool result = PackAndPostMessageHandlingError( + aCx, mPort, u"error"_ns, + aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue, + &error); + + // Step 2: Disentangle port. + // (Close() will do this) + mPort->Close(); + + // Step 3: If result is an abrupt completion, return a promise rejected with + // result.[[Value]]. + if (!result) { + return Promise::CreateRejected(mPort->GetParentObject(), error, aRv); + } + + // Step 4: Otherwise, return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv); + } + + protected: + ~CrossRealmWritableUnderlyingSinkAlgorithms() override = default; + + private: + RefPtr<SetUpTransformWritableMessageEventListener> mListener; + RefPtr<MessagePort> mPort; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(CrossRealmWritableUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase, mListener, + mPort) +NS_IMPL_ADDREF_INHERITED(CrossRealmWritableUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(CrossRealmWritableUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + CrossRealmWritableUnderlyingSinkAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSinkAlgorithmsBase) + +// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable +MOZ_CAN_RUN_SCRIPT static void SetUpCrossRealmTransformWritable( + WritableStream* aWritable, MessagePort* aPort, ErrorResult& aRv) { + // (This is only needed for step 12, but let's do this early to fail early + // enough) + AutoJSAPI jsapi; + if (!jsapi.Init(aWritable->GetParentObject())) { + return; + } + JSContext* cx = jsapi.cx(); + + // Step 1: Perform ! InitializeWritableStream(stream). + // (Done by the constructor) + + // Step 2: Let controller be a new WritableStreamDefaultController. + auto controller = MakeRefPtr<WritableStreamDefaultController>( + aWritable->GetParentObject(), *aWritable); + + // Step 3: Let backpressurePromise be a new promise. + RefPtr<Promise> backpressurePromise = + Promise::CreateInfallible(aWritable->GetParentObject()); + + // Step 4: Add a handler for port’s message event with the following steps: + auto listener = MakeRefPtr<SetUpTransformWritableMessageEventListener>( + controller, backpressurePromise); + aPort->AddEventListener(u"message"_ns, listener, false); + + // Step 5: Add a handler for port’s messageerror event with the following + // steps: + auto errorListener = + MakeRefPtr<SetUpTransformWritableMessageErrorEventListener>(controller, + aPort); + aPort->AddEventListener(u"messageerror"_ns, errorListener, false); + + // Step 6: Enable port’s port message queue. + // (Start() does it) + aPort->Start(); + + // Step 7 - 10: + auto algorithms = + MakeRefPtr<CrossRealmWritableUnderlyingSinkAlgorithms>(listener, aPort); + + // Step 11: Let sizeAlgorithm be an algorithm that returns 1. + // (nullptr should serve this purpose. See also WritableStream::Constructor) + + // Step 12: Perform ! SetUpWritableStreamDefaultController(stream, controller, + // startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, 1, + // sizeAlgorithm). + SetUpWritableStreamDefaultController(cx, aWritable, controller, algorithms, 1, + /* aSizeAlgorithm */ nullptr, aRv); +} + +class SetUpTransformReadableMessageEventListener final + : public nsIDOMEventListener { + public: + SetUpTransformReadableMessageEventListener( + ReadableStreamDefaultController* aController, MessagePort* aPort) + : mController(aController), mPort(aPort) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(SetUpTransformReadableMessageEventListener) + + // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable + // The handler steps of Step 3. + MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override { + auto cleanupPort = + MakeScopeExit([port = RefPtr<MessagePort>(mPort)]() { port->Close(); }); + + AutoJSAPI jsapi; + if (!jsapi.Init(mPort->GetParentObject())) { + return NS_OK; + } + JSContext* cx = jsapi.cx(); + MessageEvent* messageEvent = aEvent->AsMessageEvent(); + if (NS_WARN_IF(!messageEvent || !messageEvent->IsTrusted())) { + return NS_OK; + } + + // Step 1: Let data be the data of the message. + JS::Rooted<JS::Value> dataValue(cx); + IgnoredErrorResult rv; + messageEvent->GetData(cx, &dataValue, rv); + if (rv.Failed()) { + return NS_OK; + } + + // Step 2: Assert: Type(data) is Object. + // (But we check in runtime instead to avoid potential malicious events from + // a compromised process. Same below.) + if (NS_WARN_IF(!dataValue.isObject())) { + return NS_OK; + } + JS::Rooted<JSObject*> data(cx, JS::ToObject(cx, dataValue)); + + // Step 3: Let type be ! Get(data, "type"). + JS::Rooted<JS::Value> type(cx); + if (!JS_GetProperty(cx, data, "type", &type)) { + // XXX: See bug 1762233 + JS_ClearPendingException(cx); + return NS_OK; + } + + // Step 4: Let value be ! Get(data, "value"). + JS::Rooted<JS::Value> value(cx); + if (!JS_GetProperty(cx, data, "value", &value)) { + JS_ClearPendingException(cx); + return NS_OK; + } + + // Step 5: Assert: Type(type) is String. + if (NS_WARN_IF(!type.isString())) { + return NS_OK; + } + + // Step 6: If type is "chunk", + bool equals = false; + if (!JS_StringEqualsLiteral(cx, type.toString(), "chunk", &equals)) { + JS_ClearPendingException(cx); + return NS_OK; + } + if (equals) { + // Step 6.1: Perform ! ReadableStreamDefaultControllerEnqueue(controller, + // value). + ReadableStreamDefaultControllerEnqueue(cx, mController, value, + IgnoreErrors()); + cleanupPort.release(); + return NS_OK; // implicit + } + + // Step 7: Otherwise, if type is "close", + if (!JS_StringEqualsLiteral(cx, type.toString(), "close", &equals)) { + JS_ClearPendingException(cx); + return NS_OK; + } + if (equals) { + // Step 7.1: Perform ! ReadableStreamDefaultControllerClose(controller). + ReadableStreamDefaultControllerClose(cx, mController, IgnoreErrors()); + // Step 7.2: Disentangle port. + // (Close() does it) + mPort->Close(); + cleanupPort.release(); + return NS_OK; // implicit + } + + // Step 8: Otherwise, if type is "error", + if (!JS_StringEqualsLiteral(cx, type.toString(), "error", &equals)) { + JS_ClearPendingException(cx); + return NS_OK; + } + if (equals) { + // Step 8.1: Perform ! ReadableStreamDefaultControllerError(controller, + // value). + ReadableStreamDefaultControllerError(cx, mController, value, + IgnoreErrors()); + + // Step 8.2: Disentangle port. + // (Close() does it) + mPort->Close(); + cleanupPort.release(); + return NS_OK; // implicit + } + + // Logically it should be unreachable here, but we should expect random + // malicious messages. + NS_WARNING("Got an unexpected type other than chunk/close/error."); + return NS_OK; + } + + private: + ~SetUpTransformReadableMessageEventListener() = default; + + // mController never changes before CC + // TODO: MOZ_IMMUTABLE_OUTSIDE_CC + MOZ_KNOWN_LIVE RefPtr<ReadableStreamDefaultController> mController; + RefPtr<MessagePort> mPort; +}; + +NS_IMPL_CYCLE_COLLECTION(SetUpTransformReadableMessageEventListener, + mController, mPort) +NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformReadableMessageEventListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SetUpTransformReadableMessageEventListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + SetUpTransformReadableMessageEventListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +class SetUpTransformReadableMessageErrorEventListener final + : public nsIDOMEventListener { + public: + SetUpTransformReadableMessageErrorEventListener( + ReadableStreamDefaultController* aController, MessagePort* aPort) + : mController(aController), mPort(aPort) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS( + SetUpTransformReadableMessageErrorEventListener) + + // https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable + // The handler steps of Step 4. + MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override { + auto cleanupPort = + MakeScopeExit([port = RefPtr<MessagePort>(mPort)]() { port->Close(); }); + + if (NS_WARN_IF(!aEvent->AsMessageEvent() || !aEvent->IsTrusted())) { + return NS_OK; + } + + // Step 1: Let error be a new "DataCloneError" DOMException. + RefPtr<DOMException> exception = + DOMException::Create(NS_ERROR_DOM_DATA_CLONE_ERR); + + AutoJSAPI jsapi; + if (!jsapi.Init(mPort->GetParentObject())) { + return NS_OK; + } + JSContext* cx = jsapi.cx(); + JS::Rooted<JS::Value> error(cx); + if (!ToJSValue(cx, *exception, &error)) { + return NS_OK; + } + + // Step 2: Perform ! CrossRealmTransformSendError(port, error). + CrossRealmTransformSendError(cx, mPort, error); + + // Step 3: Perform ! ReadableStreamDefaultControllerError(controller, + // error). + ReadableStreamDefaultControllerError(cx, mController, error, + IgnoreErrors()); + + // Step 4: Disentangle port. + // (Close() does it) + mPort->Close(); + cleanupPort.release(); + + return NS_OK; + } + + private: + ~SetUpTransformReadableMessageErrorEventListener() = default; + + RefPtr<ReadableStreamDefaultController> mController; + RefPtr<MessagePort> mPort; +}; + +NS_IMPL_CYCLE_COLLECTION(SetUpTransformReadableMessageErrorEventListener, + mController, mPort) +NS_IMPL_CYCLE_COLLECTING_ADDREF(SetUpTransformReadableMessageErrorEventListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE( + SetUpTransformReadableMessageErrorEventListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + SetUpTransformReadableMessageErrorEventListener) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable +class CrossRealmReadableUnderlyingSourceAlgorithms final + : public UnderlyingSourceAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + CrossRealmReadableUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) + + explicit CrossRealmReadableUnderlyingSourceAlgorithms(MessagePort* aPort) + : mPort(aPort) {} + + void StartCallback(JSContext* aCx, ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override { + // Step 6. Let startAlgorithm be an algorithm that returns undefined. + aRetVal.setUndefined(); + } + + already_AddRefed<Promise> PullCallback(JSContext* aCx, + ReadableStreamController& aController, + ErrorResult& aRv) override { + // Step 7: Let pullAlgorithm be the following steps: + + // Step 7.1: Perform ! PackAndPostMessage(port, "pull", undefined). + PackAndPostMessage(aCx, mPort, u"pull"_ns, JS::UndefinedHandleValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 7.2: Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv); + } + + already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override { + // Step 8: Let cancelAlgorithm be the following steps, taking a reason + // argument: + + // Step 8.1: Let result be PackAndPostMessageHandlingError(port, "error", + // reason). + JS::Rooted<JS::Value> error(aCx); + bool result = PackAndPostMessageHandlingError( + aCx, mPort, u"error"_ns, + aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue, + &error); + + // Step 8.2: Disentangle port. + // (Close() does it) + mPort->Close(); + + // Step 8.3: If result is an abrupt completion, return a promise rejected + // with result.[[Value]]. + if (!result) { + return Promise::CreateRejected(mPort->GetParentObject(), error, aRv); + } + + // Step 8.4: Otherwise, return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(mPort->GetParentObject(), aRv); + } + + protected: + ~CrossRealmReadableUnderlyingSourceAlgorithms() override = default; + + private: + RefPtr<MessagePort> mPort; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(CrossRealmReadableUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase, mPort) +NS_IMPL_ADDREF_INHERITED(CrossRealmReadableUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(CrossRealmReadableUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + CrossRealmReadableUnderlyingSourceAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsBase) + +// https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable +MOZ_CAN_RUN_SCRIPT static void SetUpCrossRealmTransformReadable( + ReadableStream* aReadable, MessagePort* aPort, ErrorResult& aRv) { + // (This is only needed for step 10, but let's do this early to fail early + // enough) + AutoJSAPI jsapi; + if (!jsapi.Init(aReadable->GetParentObject())) { + return; + } + JSContext* cx = jsapi.cx(); + + // Step 1: Perform ! InitializeReadableStream(stream). + // (This is implicitly done by the constructor) + + // Step 2: Let controller be a new ReadableStreamDefaultController. + auto controller = + MakeRefPtr<ReadableStreamDefaultController>(aReadable->GetParentObject()); + + // Step 3: Add a handler for port’s message event with the following steps: + auto listener = + MakeRefPtr<SetUpTransformReadableMessageEventListener>(controller, aPort); + aPort->AddEventListener(u"message"_ns, listener, false); + + // Step 4: Add a handler for port’s messageerror event with the following + // steps: + auto errorListener = + MakeRefPtr<SetUpTransformReadableMessageErrorEventListener>(controller, + aPort); + aPort->AddEventListener(u"messageerror"_ns, errorListener, false); + + // Step 5: Enable port’s port message queue. + // (Start() does it) + aPort->Start(); + + // Step 6-8: + auto algorithms = + MakeRefPtr<CrossRealmReadableUnderlyingSourceAlgorithms>(aPort); + + // Step 9: Let sizeAlgorithm be an algorithm that returns 1. + // (nullptr should serve this purpose. See also ReadableStream::Constructor) + + // Step 10: Perform ! SetUpReadableStreamDefaultController(stream, controller, + // startAlgorithm, pullAlgorithm, cancelAlgorithm, 0, sizeAlgorithm). + SetUpReadableStreamDefaultController(cx, aReadable, controller, algorithms, 0, + /* aSizeAlgorithm */ nullptr, aRv); +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-steps +bool ReadableStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId) { + // Step 1: If ! IsReadableStreamLocked(value) is true, throw a + // "DataCloneError" DOMException. + // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double + // check here as the state might have changed in case this ReadableStream is + // created by a TransferStream and being transferred together with the + // parent.) + if (IsReadableStreamLocked(this)) { + return false; + } + + // Step 2: Let port1 be a new MessagePort in the current Realm. + // Step 3: Let port2 be a new MessagePort in the current Realm. + // Step 4: Entangle port1 and port2. + // (The MessageChannel constructor does exactly that.) + // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messagechannel + ErrorResult rv; + RefPtr<dom::MessageChannel> channel = + dom::MessageChannel::Constructor(mGlobal, rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + // Step 5: Let writable be a new WritableStream in the current Realm. + RefPtr<WritableStream> writable = new WritableStream( + mGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit); + + // Step 6: Perform ! SetUpCrossRealmTransformWritable(writable, port1). + // MOZ_KnownLive because Port1 never changes before CC + SetUpCrossRealmTransformWritable(writable, MOZ_KnownLive(channel->Port1()), + rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + // Step 7: Let promise be ! ReadableStreamPipeTo(value, writable, false, + // false, false). + RefPtr<Promise> promise = + ReadableStreamPipeTo(this, writable, false, false, false, nullptr, rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + // Step 8: Set promise.[[PromiseIsHandled]] to true. + MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled()); + + // Step 9: Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2, + // « port2 »). + channel->Port2()->CloneAndDisentangle(aPortId); + + return true; +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps +MOZ_CAN_RUN_SCRIPT already_AddRefed<ReadableStream> +ReadableStream::ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal, + MessagePort& aPort) { + // Step 1: Let deserializedRecord be + // ! StructuredDeserializeWithTransfer(dataHolder.[[port]], the current + // Realm). + // Step 2: Let port be deserializedRecord.[[Deserialized]]. + + // Step 3: Perform ! SetUpCrossRealmTransformReadable(value, port). + RefPtr<ReadableStream> readable = + new ReadableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + ErrorResult rv; + SetUpCrossRealmTransformReadable(readable, &aPort, rv); + if (rv.MaybeSetPendingException(aCx)) { + return nullptr; + } + return readable.forget(); +} + +bool ReadableStream::ReceiveTransfer( + JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort, + JS::MutableHandle<JSObject*> aReturnObject) { + RefPtr<ReadableStream> readable = + ReadableStream::ReceiveTransferImpl(aCx, aGlobal, aPort); + if (!readable) { + return false; + } + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, readable, &value)) { + JS_ClearPendingException(aCx); + return false; + } + aReturnObject.set(&value.toObject()); + + return true; +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-steps① +bool WritableStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId) { + // Step 1: If ! IsWritableStreamLocked(value) is true, throw a + // "DataCloneError" DOMException. + // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double + // check here as the state might have changed in case this WritableStream is + // created by a TransferStream and being transferred together with the + // parent.) + if (IsWritableStreamLocked(this)) { + return false; + } + + // Step 2: Let port1 be a new MessagePort in the current Realm. + // Step 3: Let port2 be a new MessagePort in the current Realm. + // Step 4: Entangle port1 and port2. + // (The MessageChannel constructor does exactly that.) + // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messagechannel + ErrorResult rv; + RefPtr<dom::MessageChannel> channel = + dom::MessageChannel::Constructor(mGlobal, rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + // Step 5: Let readable be a new ReadableStream in the current Realm. + RefPtr<ReadableStream> readable = new ReadableStream( + mGlobal, ReadableStream::HoldDropJSObjectsCaller::Implicit); + + // Step 6: Perform ! SetUpCrossRealmTransformReadable(readable, port1). + // MOZ_KnownLive because Port1 never changes before CC + SetUpCrossRealmTransformReadable(readable, MOZ_KnownLive(channel->Port1()), + rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + // Step 7: Let promise be ! ReadableStreamPipeTo(readable, value, false, + // false, false). + RefPtr<Promise> promise = + ReadableStreamPipeTo(readable, this, false, false, false, nullptr, rv); + if (rv.Failed()) { + return false; + } + + // Step 8: Set promise.[[PromiseIsHandled]] to true. + MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled()); + + // Step 9: Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2, + // « port2 »). + channel->Port2()->CloneAndDisentangle(aPortId); + + return true; +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps① +MOZ_CAN_RUN_SCRIPT already_AddRefed<WritableStream> +WritableStream::ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal, + MessagePort& aPort) { + // Step 1: Let deserializedRecord be ! + // StructuredDeserializeWithTransfer(dataHolder.[[port]], the current Realm). + // Step 2: Let port be a deserializedRecord.[[Deserialized]]. + + // Step 3: Perform ! SetUpCrossRealmTransformWritable(value, port). + RefPtr<WritableStream> writable = new WritableStream( + aGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit); + ErrorResult rv; + SetUpCrossRealmTransformWritable(writable, &aPort, rv); + if (rv.MaybeSetPendingException(aCx)) { + return nullptr; + } + return writable.forget(); +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps① +bool WritableStream::ReceiveTransfer( + JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort, + JS::MutableHandle<JSObject*> aReturnObject) { + RefPtr<WritableStream> writable = + WritableStream::ReceiveTransferImpl(aCx, aGlobal, aPort); + if (!writable) { + return false; + } + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, writable, &value)) { + JS_ClearPendingException(aCx); + return false; + } + aReturnObject.set(&value.toObject()); + + return true; +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-steps② +bool TransformStream::Transfer(JSContext* aCx, UniqueMessagePortId& aPortId1, + UniqueMessagePortId& aPortId2) { + // Step 1: Let readable be value.[[readable]]. + // Step 2: Let writable be value.[[writable]]. + // Step 3: If ! IsReadableStreamLocked(readable) is true, throw a + // "DataCloneError" DOMException. + // Step 4: If ! IsWritableStreamLocked(writable) is true, throw a + // "DataCloneError" DOMException. + // (Implemented in StructuredCloneHolder::CustomCanTransferHandler, but double + // check here as the state might have changed by + // Readable/WritableStream::Transfer in case the stream members of this + // TransformStream are being transferred together.) + if (IsReadableStreamLocked(mReadable) || IsWritableStreamLocked(mWritable)) { + return false; + } + + // Step 5: Set dataHolder.[[readable]] to ! + // StructuredSerializeWithTransfer(readable, « readable »). + // TODO: Mark mReadable as MOZ_KNOWN_LIVE again (bug 1769854) + if (!MOZ_KnownLive(mReadable)->Transfer(aCx, aPortId1)) { + return false; + } + + // Step 6: Set dataHolder.[[writable]] to ! + // StructuredSerializeWithTransfer(writable, « writable »). + // TODO: Mark mReadable as MOZ_KNOWN_LIVE again (bug 1769854) + return MOZ_KnownLive(mWritable)->Transfer(aCx, aPortId2); +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps② +bool TransformStream::ReceiveTransfer( + JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort1, + MessagePort& aPort2, JS::MutableHandle<JSObject*> aReturnObject) { + // Step 1: Let readableRecord be ! + // StructuredDeserializeWithTransfer(dataHolder.[[readable]], the current + // Realm). + RefPtr<ReadableStream> readable = + ReadableStream::ReceiveTransferImpl(aCx, aGlobal, aPort1); + if (!readable) { + return false; + } + + // Step 2: Let writableRecord be ! + // StructuredDeserializeWithTransfer(dataHolder.[[writable]], the current + // Realm). + RefPtr<WritableStream> writable = + WritableStream::ReceiveTransferImpl(aCx, aGlobal, aPort2); + if (!writable) { + return false; + } + + // Step 3: Set value.[[readable]] to readableRecord.[[Deserialized]]. + // Step 4: Set value.[[writable]] to writableRecord.[[Deserialized]]. + // Step 5: Set value.[[backpressure]], value.[[backpressureChangePromise]], + // and value.[[controller]] to undefined. + RefPtr<TransformStream> stream = + new TransformStream(aGlobal, readable, writable); + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, stream, &value)) { + JS_ClearPendingException(aCx); + return false; + } + aReturnObject.set(&value.toObject()); + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/streams/TransformStream.cpp b/dom/streams/TransformStream.cpp new file mode 100644 index 0000000000..e517ecda89 --- /dev/null +++ b/dom/streams/TransformStream.cpp @@ -0,0 +1,706 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/TransformStream.h" + +#include "StreamUtils.h" +#include "TransformerCallbackHelpers.h" +#include "UnderlyingSourceCallbackHelpers.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/WritableStream.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/TransformStreamBinding.h" +#include "mozilla/dom/TransformerBinding.h" +#include "nsWrapperCache.h" + +// XXX: GCC somehow does not allow attributes before lambda return types, while +// clang requires so. See also bug 1627007. +#ifdef __clang__ +# define MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA MOZ_CAN_RUN_SCRIPT_BOUNDARY +#else +# define MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA +#endif + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TransformStream, mGlobal, + mBackpressureChangePromise, mController, + mReadable, mWritable) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TransformStream) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TransformStream) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransformStream) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +// https://streams.spec.whatwg.org/#transformstream-set-up +// (except this instead creates a new TransformStream rather than accepting an +// existing instance) +already_AddRefed<TransformStream> TransformStream::CreateGeneric( + const GlobalObject& aGlobal, TransformerAlgorithmsWrapper& aAlgorithms, + ErrorResult& aRv) { + // Step 1. Let writableHighWaterMark be 1. + double writableHighWaterMark = 1; + + // Step 2. Let writableSizeAlgorithm be an algorithm that returns 1. + // Note: Callers should recognize nullptr as a callback that returns 1. See + // also WritableStream::Constructor for this design decision. + RefPtr<QueuingStrategySize> writableSizeAlgorithm; + + // Step 3. Let readableHighWaterMark be 0. + double readableHighWaterMark = 0; + + // Step 4. Let readableSizeAlgorithm be an algorithm that returns 1. + // Note: Callers should recognize nullptr as a callback that returns 1. See + // also ReadableStream::Constructor for this design decision. + RefPtr<QueuingStrategySize> readableSizeAlgorithm; + + // Step 5. Let transformAlgorithmWrapper be an algorithm that runs these steps + // given a value chunk: + // Step 6. Let flushAlgorithmWrapper be an algorithm that runs these steps: + // (Done by TransformerAlgorithmsWrapper) + + // Step 7. Let startPromise be a promise resolved with undefined. + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> startPromise = + Promise::CreateResolvedWithUndefined(global, aRv); + if (!startPromise) { + return nullptr; + } + + // Step 8. Perform ! InitializeTransformStream(stream, startPromise, + // writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, + // readableSizeAlgorithm). + RefPtr<TransformStream> stream = + new TransformStream(global, nullptr, nullptr); + stream->Initialize(aGlobal.Context(), startPromise, writableHighWaterMark, + writableSizeAlgorithm, readableHighWaterMark, + readableSizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 9. Let controller be a new TransformStreamDefaultController. + auto controller = MakeRefPtr<TransformStreamDefaultController>(global); + + // Step 10. Perform ! SetUpTransformStreamDefaultController(stream, + // controller, transformAlgorithmWrapper, flushAlgorithmWrapper). + SetUpTransformStreamDefaultController(aGlobal.Context(), *stream, *controller, + aAlgorithms); + + return stream.forget(); +} + +TransformStream::TransformStream(nsIGlobalObject* aGlobal) : mGlobal(aGlobal) { + mozilla::HoldJSObjects(this); +} + +TransformStream::TransformStream(nsIGlobalObject* aGlobal, + ReadableStream* aReadable, + WritableStream* aWritable) + : mGlobal(aGlobal), mReadable(aReadable), mWritable(aWritable) { + mozilla::HoldJSObjects(this); +} + +TransformStream::~TransformStream() { mozilla::DropJSObjects(this); } + +JSObject* TransformStream::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TransformStream_Binding::Wrap(aCx, this, aGivenProto); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#transform-stream-error-writable-and-unblock-write +void TransformStreamErrorWritableAndUnblockWrite(JSContext* aCx, + TransformStream* aStream, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1: Perform ! + // TransformStreamDefaultControllerClearAlgorithms(stream.[[controller]]). + aStream->Controller()->SetAlgorithms(nullptr); + + // Step 2: Perform ! + // WritableStreamDefaultControllerErrorIfNeeded(stream.[[writable]].[[controller]], + // e). + // TODO: Remove MOZ_KnownLive (bug 1761577) + WritableStreamDefaultControllerErrorIfNeeded( + aCx, MOZ_KnownLive(aStream->Writable()->Controller()), aError, aRv); + if (aRv.Failed()) { + return; + } + + // Step 3: If stream.[[backpressure]] is true, perform ! + // TransformStreamSetBackpressure(stream, false). + if (aStream->Backpressure()) { + aStream->SetBackpressure(false); + } +} + +// https://streams.spec.whatwg.org/#transform-stream-error +void TransformStreamError(JSContext* aCx, TransformStream* aStream, + JS::Handle<JS::Value> aError, ErrorResult& aRv) { + // Step 1: Perform ! + // ReadableStreamDefaultControllerError(stream.[[readable]].[[controller]], + // e). + ReadableStreamDefaultControllerError( + aCx, aStream->Readable()->Controller()->AsDefault(), aError, aRv); + if (aRv.Failed()) { + return; + } + + // Step 2: Perform ! TransformStreamErrorWritableAndUnblockWrite(stream, e). + TransformStreamErrorWritableAndUnblockWrite(aCx, aStream, aError, aRv); +} + +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#transform-stream-default-controller-perform-transform +MOZ_CAN_RUN_SCRIPT static already_AddRefed<Promise> +TransformStreamDefaultControllerPerformTransform( + JSContext* aCx, TransformStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1: Let transformPromise be the result of performing + // controller.[[transformAlgorithm]], passing chunk. + RefPtr<TransformerAlgorithmsBase> algorithms = aController->Algorithms(); + RefPtr<Promise> transformPromise = + algorithms->TransformCallback(aCx, aChunk, *aController, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 2: Return the result of reacting to transformPromise with the + // following rejection steps given the argument r: + auto result = transformPromise->CatchWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aError, ErrorResult& aRv, + const RefPtr<TransformStreamDefaultController>& aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA -> already_AddRefed<Promise> { + // Step 2.1: Perform ! TransformStreamError(controller.[[stream]], + // r). + // TODO: Remove MOZ_KnownLive (bug 1761577) + TransformStreamError(aCx, MOZ_KnownLive(aController->Stream()), + aError, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 2.2: Throw r. + JS::Rooted<JS::Value> r(aCx, aError); + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, r); + return nullptr; + }, + RefPtr(aController)); + if (result.isErr()) { + aRv.Throw(result.unwrapErr()); + return nullptr; + } + return result.unwrap().forget(); +} + +// https://streams.spec.whatwg.org/#initialize-transform-stream +class TransformStreamUnderlyingSinkAlgorithms final + : public UnderlyingSinkAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + TransformStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) + + TransformStreamUnderlyingSinkAlgorithms(Promise* aStartPromise, + TransformStream* aStream) + : mStartPromise(aStartPromise), mStream(aStream) {} + + void StartCallback(JSContext* aCx, + WritableStreamDefaultController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override { + // Step 1. Let startAlgorithm be an algorithm that returns startPromise. + // (Same as TransformStreamUnderlyingSourceAlgorithms::StartCallback) + aRetVal.setObject(*mStartPromise->PromiseObj()); + } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) override { + // Step 2. Let writeAlgorithm be the following steps, taking a chunk + // argument: + // Step 2.1. Return ! TransformStreamDefaultSinkWriteAlgorithm(stream, + // chunk). + + // Inlining TransformStreamDefaultSinkWriteAlgorithm here: + // https://streams.spec.whatwg.org/#transform-stream-default-sink-write-algorithm + + // Step 1: Assert: stream.[[writable]].[[state]] is "writable". + MOZ_ASSERT(mStream->Writable()->State() == + WritableStream::WriterState::Writable); + + // Step 2: Let controller be stream.[[controller]]. + RefPtr<TransformStreamDefaultController> controller = mStream->Controller(); + + // Step 3: If stream.[[backpressure]] is true, + if (mStream->Backpressure()) { + // Step 3.1: Let backpressureChangePromise be + // stream.[[backpressureChangePromise]]. + RefPtr<Promise> backpressureChangePromise = + mStream->BackpressureChangePromise(); + + // Step 3.2: Assert: backpressureChangePromise is not undefined. + MOZ_ASSERT(backpressureChangePromise); + + // Step 3.3: Return the result of reacting to backpressureChangePromise + // with the following fulfillment steps: + auto result = backpressureChangePromise->ThenWithCycleCollectedArgsJS( + [](JSContext* aCx, JS::Handle<JS::Value>, ErrorResult& aRv, + const RefPtr<TransformStream>& aStream, + const RefPtr<TransformStreamDefaultController>& aController, + JS::Handle<JS::Value> aChunk) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA -> already_AddRefed<Promise> { + // Step 1: Let writable be stream.[[writable]]. + RefPtr<WritableStream> writable = aStream->Writable(); + + // Step 2: Let state be writable.[[state]]. + WritableStream::WriterState state = writable->State(); + + // Step 3: If state is "erroring", throw + // writable.[[storedError]]. + if (state == WritableStream::WriterState::Erroring) { + JS::Rooted<JS::Value> storedError(aCx, + writable->StoredError()); + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, storedError); + return nullptr; + } + + // Step 4: Assert: state is "writable". + MOZ_ASSERT(state == WritableStream::WriterState::Writable); + + // Step 5: Return ! + // TransformStreamDefaultControllerPerformTransform(controller, + // chunk). + return TransformStreamDefaultControllerPerformTransform( + aCx, aController, aChunk, aRv); + }, + std::make_tuple(mStream, controller), std::make_tuple(aChunk)); + + if (result.isErr()) { + aRv.Throw(result.unwrapErr()); + return nullptr; + } + return result.unwrap().forget(); + } + + // Step 4: Return ! + // TransformStreamDefaultControllerPerformTransform(controller, chunk). + return TransformStreamDefaultControllerPerformTransform(aCx, controller, + aChunk, aRv); + } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override { + // Step 3. Let abortAlgorithm be the following steps, taking a reason + // argument: + // Step 3.1. Return ! TransformStreamDefaultSinkAbortAlgorithm(stream, + // reason). + + // Inlining TransformStreamDefaultSinkAbortAlgorithm here: + // https://streams.spec.whatwg.org/#transform-stream-default-sink-abort-algorithm + + // Step 1:Perform ! TransformStreamError(stream, reason). + TransformStreamError( + aCx, mStream, + aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 2: Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(mStream->GetParentObject(), + aRv); + } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseCallback( + JSContext* aCx, ErrorResult& aRv) override { + // Step 4. Let closeAlgorithm be the following steps: + // Step 4.1. Return ! TransformStreamDefaultSinkCloseAlgorithm(stream). + + // Inlining TransformStreamDefaultSinkCloseAlgorithm here: + // https://streams.spec.whatwg.org/#transform-stream-default-sink-close-algorithm + + // Step 1: Let readable be stream.[[readable]]. + RefPtr<ReadableStream> readable = mStream->Readable(); + + // Step 2: Let controller be stream.[[controller]]. + RefPtr<TransformStreamDefaultController> controller = mStream->Controller(); + + // Step 3: Let flushPromise be the result of performing + // controller.[[flushAlgorithm]]. + RefPtr<TransformerAlgorithmsBase> algorithms = controller->Algorithms(); + RefPtr<Promise> flushPromise = + algorithms->FlushCallback(aCx, *controller, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 4: Perform ! + // TransformStreamDefaultControllerClearAlgorithms(controller). + controller->SetAlgorithms(nullptr); + + // Step 5: Return the result of reacting to flushPromise: + Result<RefPtr<Promise>, nsresult> result = + flushPromise->ThenCatchWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + const RefPtr<ReadableStream>& aReadable, + const RefPtr<TransformStream>& aStream) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA + -> already_AddRefed<Promise> { + // Step 5.1: If flushPromise was fulfilled, then: + + // Step 5.1.1: If readable.[[state]] is "errored", throw + // readable.[[storedError]]. + if (aReadable->State() == + ReadableStream::ReaderState::Errored) { + JS::Rooted<JS::Value> storedError(aCx, + aReadable->StoredError()); + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, storedError); + return nullptr; + } + + // Step 5.1.2: Perform ! + // ReadableStreamDefaultControllerClose(readable.[[controller]]). + ReadableStreamDefaultControllerClose( + aCx, MOZ_KnownLive(aReadable->Controller()->AsDefault()), + aRv); + return nullptr; + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + const RefPtr<ReadableStream>& aReadable, + const RefPtr<TransformStream>& aStream) + MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA + -> already_AddRefed<Promise> { + // Step 5.2: If flushPromise was rejected with reason r, then: + + // Step 5.2.1: Perform ! TransformStreamError(stream, r). + TransformStreamError(aCx, aStream, aValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 5.2.2: Throw readable.[[storedError]]. + JS::Rooted<JS::Value> storedError(aCx, + aReadable->StoredError()); + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, storedError); + return nullptr; + }, + readable, mStream); + + if (result.isErr()) { + aRv.Throw(result.unwrapErr()); + return nullptr; + } + return result.unwrap().forget(); + } + + protected: + ~TransformStreamUnderlyingSinkAlgorithms() override = default; + + private: + RefPtr<Promise> mStartPromise; + // MOZ_KNOWN_LIVE because it won't be reassigned + MOZ_KNOWN_LIVE RefPtr<TransformStream> mStream; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TransformStreamUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase, mStartPromise, + mStream) +NS_IMPL_ADDREF_INHERITED(TransformStreamUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(TransformStreamUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransformStreamUnderlyingSinkAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSinkAlgorithmsBase) + +// https://streams.spec.whatwg.org/#initialize-transform-stream +class TransformStreamUnderlyingSourceAlgorithms final + : public UnderlyingSourceAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + TransformStreamUnderlyingSourceAlgorithms, UnderlyingSourceAlgorithmsBase) + + TransformStreamUnderlyingSourceAlgorithms(Promise* aStartPromise, + TransformStream* aStream) + : mStartPromise(aStartPromise), mStream(aStream) {} + + void StartCallback(JSContext* aCx, ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override { + // Step 1. Let startAlgorithm be an algorithm that returns startPromise. + // (Same as TransformStreamUnderlyingSinkAlgorithms::StartCallback) + aRetVal.setObject(*mStartPromise->PromiseObj()); + } + + already_AddRefed<Promise> PullCallback(JSContext* aCx, + ReadableStreamController& aController, + ErrorResult& aRv) override { + // Step 6. Let pullAlgorithm be the following steps: + // Step 6.1. Return ! TransformStreamDefaultSourcePullAlgorithm(stream). + + // Inlining TransformStreamDefaultSourcePullAlgorithm here: + // https://streams.spec.whatwg.org/#transform-stream-default-source-pull-algorithm + + // Step 1: Assert: stream.[[backpressure]] is true. + MOZ_ASSERT(mStream->Backpressure()); + + // Step 2: Assert: stream.[[backpressureChangePromise]] is not undefined. + MOZ_ASSERT(mStream->BackpressureChangePromise()); + + // Step 3: Perform ! TransformStreamSetBackpressure(stream, false). + mStream->SetBackpressure(false); + + // Step 4: Return stream.[[backpressureChangePromise]]. + return do_AddRef(mStream->BackpressureChangePromise()); + } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override { + // Step 7. Let cancelAlgorithm be the following steps, taking a reason + // argument: + // Step 7.1. Perform ! TransformStreamErrorWritableAndUnblockWrite(stream, + // reason). + TransformStreamErrorWritableAndUnblockWrite( + aCx, mStream, + aReason.WasPassed() ? aReason.Value() : JS::UndefinedHandleValue, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 7.2. Return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(mStream->GetParentObject(), + aRv); + } + + protected: + ~TransformStreamUnderlyingSourceAlgorithms() override = default; + + private: + RefPtr<Promise> mStartPromise; + // MOZ_KNOWNLIVE because it will never be reassigned + MOZ_KNOWN_LIVE RefPtr<TransformStream> mStream; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TransformStreamUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase, + mStartPromise, mStream) +NS_IMPL_ADDREF_INHERITED(TransformStreamUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(TransformStreamUnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + TransformStreamUnderlyingSourceAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsBase) + +// https://streams.spec.whatwg.org/#transform-stream-set-backpressure +void TransformStream::SetBackpressure(bool aBackpressure) { + // Step 1. Assert: stream.[[backpressure]] is not backpressure. + MOZ_ASSERT(Backpressure() != aBackpressure); + + // Step 2. If stream.[[backpressureChangePromise]] is not undefined, resolve + // stream.[[backpressureChangePromise]] with undefined. + if (Promise* promise = BackpressureChangePromise()) { + promise->MaybeResolveWithUndefined(); + } + + // Step 3. Set stream.[[backpressureChangePromise]] to a new promise. + RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject()); + mBackpressureChangePromise = promise; + + // Step 4. Set stream.[[backpressure]] to backpressure. + mBackpressure = aBackpressure; +} + +// https://streams.spec.whatwg.org/#initialize-transform-stream +void TransformStream::Initialize(JSContext* aCx, Promise* aStartPromise, + double aWritableHighWaterMark, + QueuingStrategySize* aWritableSizeAlgorithm, + double aReadableHighWaterMark, + QueuingStrategySize* aReadableSizeAlgorithm, + ErrorResult& aRv) { + // Step 1 - 4 + auto sinkAlgorithms = + MakeRefPtr<TransformStreamUnderlyingSinkAlgorithms>(aStartPromise, this); + + // Step 5. Set stream.[[writable]] to ! CreateWritableStream(startAlgorithm, + // writeAlgorithm, closeAlgorithm, abortAlgorithm, writableHighWaterMark, + // writableSizeAlgorithm). + mWritable = WritableStream::CreateAbstract( + aCx, MOZ_KnownLive(mGlobal), sinkAlgorithms, aWritableHighWaterMark, + aWritableSizeAlgorithm, aRv); + if (aRv.Failed()) { + return; + } + + // Step 6 - 7 + auto sourceAlgorithms = MakeRefPtr<TransformStreamUnderlyingSourceAlgorithms>( + aStartPromise, this); + + // Step 8. Set stream.[[readable]] to ! CreateReadableStream(startAlgorithm, + // pullAlgorithm, cancelAlgorithm, readableHighWaterMark, + // readableSizeAlgorithm). + mReadable = ReadableStream::CreateAbstract( + aCx, MOZ_KnownLive(mGlobal), sourceAlgorithms, + Some(aReadableHighWaterMark), aReadableSizeAlgorithm, aRv); + if (aRv.Failed()) { + return; + } + + // Step 9. Set stream.[[backpressure]] and + // stream.[[backpressureChangePromise]] to undefined. + // Note(krosylight): The spec allows setting [[backpressure]] as undefined, + // but I don't see why it should be. Since the spec also allows strict boolean + // type, and this is only to not trigger assertion inside the setter, we just + // set it as false. + mBackpressure = false; + mBackpressureChangePromise = nullptr; + + // Step 10. Perform ! TransformStreamSetBackpressure(stream, true). + SetBackpressure(true); + if (aRv.Failed()) { + return; + } + + // Step 11. Set stream.[[controller]] to undefined. + mController = nullptr; +} + +// https://streams.spec.whatwg.org/#ts-constructor +already_AddRefed<TransformStream> TransformStream::Constructor( + const GlobalObject& aGlobal, + const Optional<JS::Handle<JSObject*>>& aTransformer, + const QueuingStrategy& aWritableStrategy, + const QueuingStrategy& aReadableStrategy, ErrorResult& aRv) { + // Step 1. If transformer is missing, set it to null. + JS::Rooted<JSObject*> transformerObj( + aGlobal.Context(), + aTransformer.WasPassed() ? aTransformer.Value() : nullptr); + + // Step 2. Let transformerDict be transformer, converted to an IDL value of + // type Transformer. + RootedDictionary<Transformer> transformerDict(aGlobal.Context()); + if (transformerObj) { + JS::Rooted<JS::Value> objValue(aGlobal.Context(), + JS::ObjectValue(*transformerObj)); + dom::BindingCallContext callCx(aGlobal.Context(), + "TransformStream.constructor"); + aRv.MightThrowJSException(); + if (!transformerDict.Init(callCx, objValue)) { + aRv.StealExceptionFromJSContext(aGlobal.Context()); + return nullptr; + } + } + + // Step 3. If transformerDict["readableType"] exists, throw a RangeError + // exception. + if (!transformerDict.mReadableType.isUndefined()) { + aRv.ThrowRangeError( + "`readableType` is unsupported and preserved for future use"); + return nullptr; + } + + // Step 4. If transformerDict["writableType"] exists, throw a RangeError + // exception. + if (!transformerDict.mWritableType.isUndefined()) { + aRv.ThrowRangeError( + "`writableType` is unsupported and preserved for future use"); + return nullptr; + } + + // Step 5. Let readableHighWaterMark be ? + // ExtractHighWaterMark(readableStrategy, 0). + double readableHighWaterMark = + ExtractHighWaterMark(aReadableStrategy, 0, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 6. Let readableSizeAlgorithm be ! + // ExtractSizeAlgorithm(readableStrategy). + // Note: Callers should recognize nullptr as a callback that returns 1. See + // also ReadableStream::Constructor for this design decision. + RefPtr<QueuingStrategySize> readableSizeAlgorithm = + aReadableStrategy.mSize.WasPassed() ? &aReadableStrategy.mSize.Value() + : nullptr; + + // Step 7. Let writableHighWaterMark be ? + // ExtractHighWaterMark(writableStrategy, 1). + double writableHighWaterMark = + ExtractHighWaterMark(aWritableStrategy, 1, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 8. Let writableSizeAlgorithm be ! + // ExtractSizeAlgorithm(writableStrategy). + // Note: Callers should recognize nullptr as a callback that returns 1. See + // also WritableStream::Constructor for this design decision. + RefPtr<QueuingStrategySize> writableSizeAlgorithm = + aWritableStrategy.mSize.WasPassed() ? &aWritableStrategy.mSize.Value() + : nullptr; + + // Step 9. Let startPromise be a new promise. + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> startPromise = Promise::CreateInfallible(global); + + // Step 10. Perform ! InitializeTransformStream(this, startPromise, + // writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, + // readableSizeAlgorithm). + RefPtr<TransformStream> transformStream = new TransformStream(global); + transformStream->Initialize( + aGlobal.Context(), startPromise, writableHighWaterMark, + writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 11. Perform ? + // SetUpTransformStreamDefaultControllerFromTransformer(this, transformer, + // transformerDict). + SetUpTransformStreamDefaultControllerFromTransformer( + aGlobal.Context(), *transformStream, transformerObj, transformerDict); + + // Step 12. If transformerDict["start"] exists, then resolve startPromise with + // the result of invoking transformerDict["start"] with argument list « + // this.[[controller]] » and callback this value transformer. + if (transformerDict.mStart.WasPassed()) { + RefPtr<TransformerStartCallback> callback = transformerDict.mStart.Value(); + RefPtr<TransformStreamDefaultController> controller = + transformStream->Controller(); + JS::Rooted<JS::Value> retVal(aGlobal.Context()); + callback->Call(transformerObj, *controller, &retVal, aRv, + "Transformer.start", CallbackFunction::eRethrowExceptions); + if (aRv.Failed()) { + return nullptr; + } + + startPromise->MaybeResolve(retVal); + } else { + // Step 13. Otherwise, resolve startPromise with undefined. + startPromise->MaybeResolveWithUndefined(); + } + + return transformStream.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/streams/TransformStream.h b/dom/streams/TransformStream.h new file mode 100644 index 0000000000..ed9ec6f812 --- /dev/null +++ b/dom/streams/TransformStream.h @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_STREAMS_TRANSFORMSTREAM_H_ +#define DOM_STREAMS_TRANSFORMSTREAM_H_ + +#include "TransformStreamDefaultController.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" + +#include "mozilla/dom/TransformerBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class WritableStream; +class ReadableStream; +class UniqueMessagePortId; +class MessagePort; +class TransformerAlgorithmsWrapper; + +class TransformStream final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TransformStream) + + // https://streams.spec.whatwg.org/#transformstream-set-up + // Intended to be used by interfaces using GenericTransformStream mixin. + MOZ_CAN_RUN_SCRIPT static already_AddRefed<TransformStream> CreateGeneric( + const GlobalObject& aGlobal, TransformerAlgorithmsWrapper& aAlgorithms, + ErrorResult& aRv); + + // Internal slot accessors + bool Backpressure() const { return mBackpressure; } + Promise* BackpressureChangePromise() { return mBackpressureChangePromise; } + void SetBackpressure(bool aBackpressure); + MOZ_KNOWN_LIVE TransformStreamDefaultController* Controller() { + return mController; + } + void SetController(TransformStreamDefaultController& aController) { + MOZ_ASSERT(!mController); + mController = &aController; + } + + // [Transferable] + // https://html.spec.whatwg.org/multipage/structured-data.html#transfer-steps + MOZ_CAN_RUN_SCRIPT bool Transfer(JSContext* aCx, + UniqueMessagePortId& aPortId1, + UniqueMessagePortId& aPortId2); + // https://html.spec.whatwg.org/multipage/structured-data.html#transfer-receiving-steps + static MOZ_CAN_RUN_SCRIPT bool ReceiveTransfer( + JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort1, + MessagePort& aPort2, JS::MutableHandle<JSObject*> aReturnObject); + + protected: + TransformStream(nsIGlobalObject* aGlobal, ReadableStream* aReadable, + WritableStream* aWritable); + explicit TransformStream(nsIGlobalObject* aGlobal); + + ~TransformStream(); + + MOZ_CAN_RUN_SCRIPT void Initialize( + JSContext* aCx, Promise* aStartPromise, double aWritableHighWaterMark, + QueuingStrategySize* aWritableSizeAlgorithm, + double aReadableHighWaterMark, + QueuingStrategySize* aReadableSizeAlgorithm, ErrorResult& aRv); + + public: + nsIGlobalObject* GetParentObject() const { return mGlobal; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL methods + // TODO: Mark as MOZ_CAN_RUN_SCRIPT when IDL constructors can be (bug 1749042) + MOZ_CAN_RUN_SCRIPT_BOUNDARY static already_AddRefed<TransformStream> + Constructor(const GlobalObject& aGlobal, + const Optional<JS::Handle<JSObject*>>& aTransformer, + const QueuingStrategy& aWritableStrategy, + const QueuingStrategy& aReadableStrategy, ErrorResult& aRv); + + ReadableStream* Readable() const { return mReadable; } + WritableStream* Writable() const { return mWritable; } + + private: + nsCOMPtr<nsIGlobalObject> mGlobal; + + // Internal slots + // MOZ_KNOWN_LIVE for slots that will never be reassigned + bool mBackpressure = false; + RefPtr<Promise> mBackpressureChangePromise; + RefPtr<TransformStreamDefaultController> mController; + RefPtr<ReadableStream> mReadable; + RefPtr<WritableStream> mWritable; +}; + +namespace streams_abstract { + +MOZ_CAN_RUN_SCRIPT void TransformStreamErrorWritableAndUnblockWrite( + JSContext* aCx, TransformStream* aStream, JS::Handle<JS::Value> aError, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void TransformStreamError(JSContext* aCx, + TransformStream* aStream, + JS::Handle<JS::Value> aError, + ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // DOM_STREAMS_TRANSFORMSTREAM_H_ diff --git a/dom/streams/TransformStreamDefaultController.cpp b/dom/streams/TransformStreamDefaultController.cpp new file mode 100644 index 0000000000..bbcb7f94f2 --- /dev/null +++ b/dom/streams/TransformStreamDefaultController.cpp @@ -0,0 +1,238 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/TransformStreamDefaultController.h" + +#include "TransformerCallbackHelpers.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/TransformStream.h" +#include "mozilla/dom/TransformStreamDefaultControllerBinding.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TransformStreamDefaultController, mGlobal, + mStream, mTransformerAlgorithms) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TransformStreamDefaultController) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TransformStreamDefaultController) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransformStreamDefaultController) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TransformStream* TransformStreamDefaultController::Stream() { return mStream; } + +void TransformStreamDefaultController::SetStream(TransformStream& aStream) { + MOZ_ASSERT(!mStream); + mStream = &aStream; +} + +TransformerAlgorithmsBase* TransformStreamDefaultController::Algorithms() { + return mTransformerAlgorithms; +} + +void TransformStreamDefaultController::SetAlgorithms( + TransformerAlgorithmsBase* aTransformerAlgorithms) { + mTransformerAlgorithms = aTransformerAlgorithms; +} + +TransformStreamDefaultController::TransformStreamDefaultController( + nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) { + mozilla::HoldJSObjects(this); +} + +TransformStreamDefaultController::~TransformStreamDefaultController() { + mozilla::DropJSObjects(this); +} + +JSObject* TransformStreamDefaultController::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return TransformStreamDefaultController_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://streams.spec.whatwg.org/#ts-default-controller-desired-size +Nullable<double> TransformStreamDefaultController::GetDesiredSize() const { + // Step 1. Let readableController be + // this.[[stream]].[[readable]].[[controller]]. + RefPtr<ReadableStreamDefaultController> readableController = + mStream->Readable()->Controller()->AsDefault(); + + // Step 2. Return ! + // ReadableStreamDefaultControllerGetDesiredSize(readableController). + return ReadableStreamDefaultControllerGetDesiredSize(readableController); +} + +// https://streams.spec.whatwg.org/#rs-default-controller-has-backpressure +// Looks like a readable stream thing but the spec explicitly says this is for +// TransformStream. +static bool ReadableStreamDefaultControllerHasBackpressure( + ReadableStreamDefaultController* aController) { + // Step 1: If ! ReadableStreamDefaultControllerShouldCallPull(controller) is + // true, return false. + // Step 2: Otherwise, return true. + return !ReadableStreamDefaultControllerShouldCallPull(aController); +} + +void TransformStreamDefaultController::Enqueue(JSContext* aCx, + JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) { + // Step 1: Perform ? TransformStreamDefaultControllerEnqueue(this, chunk). + + // Inlining TransformStreamDefaultControllerEnqueue here. + // https://streams.spec.whatwg.org/#transform-stream-default-controller-enqueue + + // Step 1: Let stream be controller.[[stream]]. + RefPtr<TransformStream> stream = mStream; + + // Step 2: Let readableController be stream.[[readable]].[[controller]]. + RefPtr<ReadableStreamDefaultController> readableController = + stream->Readable()->Controller()->AsDefault(); + + // Step 3: If ! + // ReadableStreamDefaultControllerCanCloseOrEnqueue(readableController) is + // false, throw a TypeError exception. + if (!ReadableStreamDefaultControllerCanCloseOrEnqueueAndThrow( + readableController, CloseOrEnqueue::Enqueue, aRv)) { + return; + } + + // Step 4: Let enqueueResult be + // ReadableStreamDefaultControllerEnqueue(readableController, chunk). + ErrorResult rv; + ReadableStreamDefaultControllerEnqueue(aCx, readableController, aChunk, rv); + + // Step 5: If enqueueResult is an abrupt completion, + if (rv.MaybeSetPendingException(aCx)) { + JS::Rooted<JS::Value> error(aCx); + if (!JS_GetPendingException(aCx, &error)) { + // Uncatchable exception; we should mark aRv and return. + aRv.StealExceptionFromJSContext(aCx); + return; + } + JS_ClearPendingException(aCx); + + // Step 5.1: Perform ! TransformStreamErrorWritableAndUnblockWrite(stream, + // enqueueResult.[[Value]]). + TransformStreamErrorWritableAndUnblockWrite(aCx, stream, error, aRv); + + // Step 5.2: Throw stream.[[readable]].[[storedError]]. + JS::Rooted<JS::Value> storedError(aCx, stream->Readable()->StoredError()); + aRv.MightThrowJSException(); + aRv.ThrowJSException(aCx, storedError); + return; + } + + // Step 6: Let backpressure be ! + // ReadableStreamDefaultControllerHasBackpressure(readableController). + bool backpressure = + ReadableStreamDefaultControllerHasBackpressure(readableController); + + // Step 7: If backpressure is not stream.[[backpressure]], + if (backpressure != stream->Backpressure()) { + // Step 7.1: Assert: backpressure is true. + MOZ_ASSERT(backpressure); + + // Step 7.2: Perform ! TransformStreamSetBackpressure(true). + stream->SetBackpressure(true); + } +} + +// https://streams.spec.whatwg.org/#ts-default-controller-error +void TransformStreamDefaultController::Error(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1: Perform ? TransformStreamDefaultControllerError(this, e). + + // Inlining TransformStreamDefaultControllerError here. + // https://streams.spec.whatwg.org/#transform-stream-default-controller-error + + // Perform ! TransformStreamError(controller.[[stream]], e). + // mStream is set in initialization step and only modified in cycle + // collection. + // TODO: Move mStream initialization to a method/constructor and make it + // MOZ_KNOWN_LIVE again. (See bug 1769854) + TransformStreamError(aCx, MOZ_KnownLive(mStream), aError, aRv); +} + +// https://streams.spec.whatwg.org/#ts-default-controller-terminate + +void TransformStreamDefaultController::Terminate(JSContext* aCx, + ErrorResult& aRv) { + // Step 1: Perform ? TransformStreamDefaultControllerTerminate(this). + + // Inlining TransformStreamDefaultControllerTerminate here. + // https://streams.spec.whatwg.org/#transform-stream-default-controller-terminate + + // Step 1: Let stream be controller.[[stream]]. + RefPtr<TransformStream> stream = mStream; + + // Step 2: Let readableController be stream.[[readable]].[[controller]]. + RefPtr<ReadableStreamDefaultController> readableController = + stream->Readable()->Controller()->AsDefault(); + + // Step 3: Perform ! ReadableStreamDefaultControllerClose(readableController). + ReadableStreamDefaultControllerClose(aCx, readableController, aRv); + + // Step 4: Let error be a TypeError exception indicating that the stream has + // been terminated. + ErrorResult rv; + rv.ThrowTypeError("Terminating the stream"); + JS::Rooted<JS::Value> error(aCx); + MOZ_ALWAYS_TRUE(ToJSValue(aCx, std::move(rv), &error)); + + // Step 5: Perform ! TransformStreamErrorWritableAndUnblockWrite(stream, + // error). + TransformStreamErrorWritableAndUnblockWrite(aCx, stream, error, aRv); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller +void SetUpTransformStreamDefaultController( + JSContext* aCx, TransformStream& aStream, + TransformStreamDefaultController& aController, + TransformerAlgorithmsBase& aTransformerAlgorithms) { + // Step 1. Assert: stream implements TransformStream. + // Step 2. Assert: stream.[[controller]] is undefined. + MOZ_ASSERT(!aStream.Controller()); + + // Step 3. Set controller.[[stream]] to stream. + aController.SetStream(aStream); + + // Step 4. Set stream.[[controller]] to controller. + aStream.SetController(aController); + + // Step 5. Set controller.[[transformAlgorithm]] to transformAlgorithm. + // Step 6. Set controller.[[flushAlgorithm]] to flushAlgorithm. + aController.SetAlgorithms(&aTransformerAlgorithms); +} + +// https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller-from-transformer +void SetUpTransformStreamDefaultControllerFromTransformer( + JSContext* aCx, TransformStream& aStream, + JS::Handle<JSObject*> aTransformer, Transformer& aTransformerDict) { + // Step 1. Let controller be a new TransformStreamDefaultController. + auto controller = + MakeRefPtr<TransformStreamDefaultController>(aStream.GetParentObject()); + + // Step 2 - 5: + auto algorithms = MakeRefPtr<TransformerAlgorithms>( + aStream.GetParentObject(), aTransformer, aTransformerDict); + + // Step 6: Perform ! SetUpTransformStreamDefaultController(stream, controller, + // transformAlgorithm, flushAlgorithm). + SetUpTransformStreamDefaultController(aCx, aStream, *controller, *algorithms); +} + +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/TransformStreamDefaultController.h b/dom/streams/TransformStreamDefaultController.h new file mode 100644 index 0000000000..71b92f69fe --- /dev/null +++ b/dom/streams/TransformStreamDefaultController.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_STREAMS_TRANSFORMSTREAMDEFAULTCONTROLLER_H_ +#define DOM_STREAMS_TRANSFORMSTREAMDEFAULTCONTROLLER_H_ + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" + +#include "mozilla/dom/TransformerBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class TransformStream; +class TransformerAlgorithmsBase; + +class TransformStreamDefaultController final : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TransformStreamDefaultController) + + MOZ_KNOWN_LIVE TransformStream* Stream(); + void SetStream(TransformStream& aStream); + TransformerAlgorithmsBase* Algorithms(); + void SetAlgorithms(TransformerAlgorithmsBase* aTransformerAlgorithms); + + explicit TransformStreamDefaultController(nsIGlobalObject* aGlobal); + + protected: + ~TransformStreamDefaultController(); + + public: + nsIGlobalObject* GetParentObject() const { return mGlobal; } + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + Nullable<double> GetDesiredSize() const; + + MOZ_CAN_RUN_SCRIPT void Enqueue(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void Error(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void Terminate(JSContext* aCx, ErrorResult& aRv); + + private: + nsCOMPtr<nsIGlobalObject> mGlobal; + + // Internal slots + RefPtr<TransformStream> mStream; + RefPtr<TransformerAlgorithmsBase> mTransformerAlgorithms; +}; + +namespace streams_abstract { +void SetUpTransformStreamDefaultController( + JSContext* aCx, TransformStream& aStream, + TransformStreamDefaultController& aController, + TransformerAlgorithmsBase& aTransformerAlgorithms); + +void SetUpTransformStreamDefaultControllerFromTransformer( + JSContext* aCx, TransformStream& aStream, + JS::Handle<JSObject*> aTransformer, Transformer& aTransformerDict); +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // DOM_STREAMS_TRANSFORMSTREAMDEFAULTCONTROLLER_H_ diff --git a/dom/streams/TransformerCallbackHelpers.cpp b/dom/streams/TransformerCallbackHelpers.cpp new file mode 100644 index 0000000000..606c24adda --- /dev/null +++ b/dom/streams/TransformerCallbackHelpers.cpp @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TransformerCallbackHelpers.h" + +#include "StreamUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/TransformStreamDefaultController.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION(TransformerAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TransformerAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TransformerAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransformerAlgorithmsBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_INHERITED_WITH_JS_MEMBERS( + TransformerAlgorithms, TransformerAlgorithmsBase, + (mGlobal, mTransformCallback, mFlushCallback), (mTransformer)) +NS_IMPL_ADDREF_INHERITED(TransformerAlgorithms, TransformerAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(TransformerAlgorithms, TransformerAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransformerAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(TransformerAlgorithmsBase) + +// https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller-from-transformer +already_AddRefed<Promise> TransformerAlgorithms::TransformCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + TransformStreamDefaultController& aController, ErrorResult& aRv) { + if (!mTransformCallback) { + // Step 2.1. Let result be + // TransformStreamDefaultControllerEnqueue(controller, chunk). + aController.Enqueue(aCx, aChunk, aRv); + + // Step 2.2. If result is an abrupt completion, return a promise rejected + // with result.[[Value]]. + if (aRv.MaybeSetPendingException(aCx)) { + JS::Rooted<JS::Value> error(aCx); + if (!JS_GetPendingException(aCx, &error)) { + // Uncatchable exception; we should mark aRv and return. + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + JS_ClearPendingException(aCx); + + return Promise::CreateRejected(aController.GetParentObject(), error, aRv); + } + + // Step 2.3. Otherwise, return a promise resolved with undefined. + return Promise::CreateResolvedWithUndefined(aController.GetParentObject(), + aRv); + } + // Step 4. If transformerDict["transform"] exists, set transformAlgorithm to + // an algorithm which takes an argument chunk and returns the result of + // invoking transformerDict["transform"] with argument list « chunk, + // controller » and callback this value transformer. + JS::Rooted<JSObject*> thisObj(aCx, mTransformer); + return MOZ_KnownLive(mTransformCallback) + ->Call(thisObj, aChunk, aController, aRv, + "TransformStreamDefaultController.[[transformAlgorithm]]", + CallbackObject::eRethrowExceptions); +} + +// https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller-from-transformer +already_AddRefed<Promise> TransformerAlgorithms::FlushCallback( + JSContext* aCx, TransformStreamDefaultController& aController, + ErrorResult& aRv) { + if (!mFlushCallback) { + // Step 3. Let flushAlgorithm be an algorithm which returns a promise + // resolved with undefined. + return Promise::CreateResolvedWithUndefined(aController.GetParentObject(), + aRv); + } + // Step 5. If transformerDict["flush"] exists, set flushAlgorithm to an + // algorithm which returns the result of invoking transformerDict["flush"] + // with argument list « controller » and callback this value transformer. + JS::Rooted<JSObject*> thisObj(aCx, mTransformer); + return MOZ_KnownLive(mFlushCallback) + ->Call(thisObj, aController, aRv, + "TransformStreamDefaultController.[[flushAlgorithm]]", + CallbackObject::eRethrowExceptions); +} + +already_AddRefed<Promise> TransformerAlgorithmsWrapper::TransformCallback( + JSContext*, JS::Handle<JS::Value> aChunk, + TransformStreamDefaultController& aController, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = aController.GetParentObject(); + return PromisifyAlgorithm( + global, + [this, &aChunk, &aController](ErrorResult& aRv) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + return TransformCallbackImpl(aChunk, aController, aRv); + }, + aRv); +} + +already_AddRefed<Promise> TransformerAlgorithmsWrapper::FlushCallback( + JSContext*, TransformStreamDefaultController& aController, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = aController.GetParentObject(); + return PromisifyAlgorithm( + global, + [this, &aController](ErrorResult& aRv) MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + return FlushCallbackImpl(aController, aRv); + }, + aRv); +} diff --git a/dom/streams/TransformerCallbackHelpers.h b/dom/streams/TransformerCallbackHelpers.h new file mode 100644 index 0000000000..8b12a31b0f --- /dev/null +++ b/dom/streams/TransformerCallbackHelpers.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DOM_STREAMS_TRANSFORMERCALLBACKHELPERS_H_ +#define DOM_STREAMS_TRANSFORMERCALLBACKHELPERS_H_ + +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/TransformerBinding.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { + +class Promise; + +class TransformerAlgorithmsBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(TransformerAlgorithmsBase) + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> TransformCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + TransformStreamDefaultController& aController, ErrorResult& aRv) = 0; + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> FlushCallback( + JSContext* aCx, TransformStreamDefaultController& aController, + ErrorResult& aRv) = 0; + + protected: + virtual ~TransformerAlgorithmsBase() = default; +}; + +// https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller-from-transformer +class TransformerAlgorithms final : public TransformerAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + TransformerAlgorithms, TransformerAlgorithmsBase) + + TransformerAlgorithms(nsIGlobalObject* aGlobal, + JS::Handle<JSObject*> aTransformer, + Transformer& aTransformerDict) + : mGlobal(aGlobal), mTransformer(aTransformer) { + // Step 4. (Step 2 is implicitly done through the initialization of + // mTransformCallback to null) + if (aTransformerDict.mTransform.WasPassed()) { + mTransformCallback = aTransformerDict.mTransform.Value(); + } + + // Step 5. (Step 3 is implicitly done through the initialization of + // mTransformCallback to null) + if (aTransformerDict.mFlush.WasPassed()) { + mFlushCallback = aTransformerDict.mFlush.Value(); + } + + mozilla::HoldJSObjects(this); + }; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> TransformCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + TransformStreamDefaultController& aController, ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> FlushCallback( + JSContext* aCx, TransformStreamDefaultController& aController, + ErrorResult& aRv) override; + + protected: + ~TransformerAlgorithms() { mozilla::DropJSObjects(this); } + + private: + // Virtually const, but are cycle collected + nsCOMPtr<nsIGlobalObject> mGlobal; + JS::Heap<JSObject*> mTransformer; + MOZ_KNOWN_LIVE RefPtr<TransformerTransformCallback> mTransformCallback; + MOZ_KNOWN_LIVE RefPtr<TransformerFlushCallback> mFlushCallback; +}; + +// https://streams.spec.whatwg.org/#transformstream-set-up +class TransformerAlgorithmsWrapper : public TransformerAlgorithmsBase { + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> TransformCallback( + JSContext*, JS::Handle<JS::Value> aChunk, + TransformStreamDefaultController& aController, ErrorResult& aRv) final; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> FlushCallback( + JSContext*, TransformStreamDefaultController& aController, + ErrorResult& aRv) final; + + MOZ_CAN_RUN_SCRIPT virtual void TransformCallbackImpl( + JS::Handle<JS::Value> aChunk, + TransformStreamDefaultController& aController, ErrorResult& aRv) = 0; + + MOZ_CAN_RUN_SCRIPT virtual void FlushCallbackImpl( + TransformStreamDefaultController& aController, ErrorResult& aRv) { + // flushAlgorithm is optional, do nothing by default + } +}; + +} // namespace mozilla::dom + +#endif // DOM_STREAMS_TRANSFORMERCALLBACKHELPERS_H_ diff --git a/dom/streams/UnderlyingSinkCallbackHelpers.cpp b/dom/streams/UnderlyingSinkCallbackHelpers.cpp new file mode 100644 index 0000000000..91562a2db3 --- /dev/null +++ b/dom/streams/UnderlyingSinkCallbackHelpers.cpp @@ -0,0 +1,274 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/UnderlyingSinkCallbackHelpers.h" +#include "StreamUtils.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/WebTransportError.h" +#include "nsHttp.h" + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION(UnderlyingSinkAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTING_ADDREF(UnderlyingSinkAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(UnderlyingSinkAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UnderlyingSinkAlgorithmsBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_INHERITED_WITH_JS_MEMBERS( + UnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase, + (mGlobal, mStartCallback, mWriteCallback, mCloseCallback, mAbortCallback), + (mUnderlyingSink)) +NS_IMPL_ADDREF_INHERITED(UnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(UnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UnderlyingSinkAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSinkAlgorithmsBase) + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink +void UnderlyingSinkAlgorithms::StartCallback( + JSContext* aCx, WritableStreamDefaultController& aController, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) { + if (!mStartCallback) { + // Step 2: Let startAlgorithm be an algorithm that returns undefined. + aRetVal.setUndefined(); + return; + } + + // Step 6: If underlyingSinkDict["start"] exists, then set startAlgorithm to + // an algorithm which returns the result of invoking + // underlyingSinkDict["start"] with argument list « controller » and callback + // this value underlyingSink. + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSink); + return mStartCallback->Call(thisObj, aController, aRetVal, aRv, + "UnderlyingSink.start", + CallbackFunction::eRethrowExceptions); +} + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink +already_AddRefed<Promise> UnderlyingSinkAlgorithms::WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) { + if (!mWriteCallback) { + // Step 3: Let writeAlgorithm be an algorithm that returns a promise + // resolved with undefined. + return Promise::CreateResolvedWithUndefined(mGlobal, aRv); + } + + // Step 7: If underlyingSinkDict["write"] exists, then set writeAlgorithm to + // an algorithm which takes an argument chunk and returns the result of + // invoking underlyingSinkDict["write"] with argument list « chunk, controller + // » and callback this value underlyingSink. + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSink); + RefPtr<Promise> promise = mWriteCallback->Call( + thisObj, aChunk, aController, aRv, "UnderlyingSink.write", + CallbackFunction::eRethrowExceptions); + return promise.forget(); +} + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink +already_AddRefed<Promise> UnderlyingSinkAlgorithms::CloseCallback( + JSContext* aCx, ErrorResult& aRv) { + if (!mCloseCallback) { + // Step 4: Let closeAlgorithm be an algorithm that returns a promise + // resolved with undefined. + return Promise::CreateResolvedWithUndefined(mGlobal, aRv); + } + + // Step 8: If underlyingSinkDict["close"] exists, then set closeAlgorithm to + // an algorithm which returns the result of invoking + // underlyingSinkDict["close"] with argument list «» and callback this value + // underlyingSink. + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSink); + RefPtr<Promise> promise = + mCloseCallback->Call(thisObj, aRv, "UnderlyingSink.close", + CallbackFunction::eRethrowExceptions); + return promise.forget(); +} + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink +already_AddRefed<Promise> UnderlyingSinkAlgorithms::AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + if (!mAbortCallback) { + // Step 5: Let abortAlgorithm be an algorithm that returns a promise + // resolved with undefined. + return Promise::CreateResolvedWithUndefined(mGlobal, aRv); + } + + // Step 9: Let abortAlgorithm be an algorithm that returns a promise resolved + // with undefined. + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSink); + RefPtr<Promise> promise = + mAbortCallback->Call(thisObj, aReason, aRv, "UnderlyingSink.abort", + CallbackFunction::eRethrowExceptions); + + return promise.forget(); +} + +// https://streams.spec.whatwg.org/#writable-set-up +// Step 2.1: Let closeAlgorithmWrapper be an algorithm that runs these steps: +already_AddRefed<Promise> UnderlyingSinkAlgorithmsWrapper::CloseCallback( + JSContext* aCx, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx); + return PromisifyAlgorithm( + global, [&](ErrorResult& aRv) { return CloseCallbackImpl(aCx, aRv); }, + aRv); +} + +// https://streams.spec.whatwg.org/#writable-set-up +// Step 3.1: Let abortAlgorithmWrapper be an algorithm that runs these steps: +already_AddRefed<Promise> UnderlyingSinkAlgorithmsWrapper::AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx); + return PromisifyAlgorithm( + global, + [&](ErrorResult& aRv) { return AbortCallbackImpl(aCx, aReason, aRv); }, + aRv); +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(WritableStreamToOutput, + UnderlyingSinkAlgorithmsBase, + nsIOutputStreamCallback) +NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableStreamToOutput, + UnderlyingSinkAlgorithmsBase, mParent, + mOutput, mPromise) + +NS_IMETHODIMP +WritableStreamToOutput::OnOutputStreamReady(nsIAsyncOutputStream* aStream) { + if (!mData) { + return NS_OK; + } + MOZ_ASSERT(mPromise); + uint32_t written = 0; + nsresult rv = mOutput->Write( + reinterpret_cast<const char*>(mData->Elements() + mWritten), + mData->Length() - mWritten, &written); + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + mPromise->MaybeRejectWithAbortError("Error writing to stream"_ns); + ClearData(); + // XXX should we add mErrored and fail future calls immediately? + // I presume new calls to Write() will fail though, too + return rv; + } + if (NS_SUCCEEDED(rv)) { + mWritten += written; + MOZ_ASSERT(mWritten <= mData->Length()); + if (mWritten >= mData->Length()) { + mPromise->MaybeResolveWithUndefined(); + ClearData(); + return NS_OK; + } + // more to write + } + // wrote partial or nothing + // Wait for space + nsCOMPtr<nsIEventTarget> target = mozilla::GetCurrentSerialEventTarget(); + rv = mOutput->AsyncWait(this, 0, 0, target); + if (NS_FAILED(rv)) { + mPromise->MaybeRejectWithUnknownError("error waiting to write data"); + ClearData(); + // XXX should we add mErrored and fail future calls immediately? + // New calls to Write() will fail, note + // See step 5.2 of + // https://streams.spec.whatwg.org/#writable-stream-default-controller-process-write. + return rv; + } + return NS_OK; +} + +already_AddRefed<Promise> WritableStreamToOutput::WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aError) { + ArrayBufferViewOrArrayBuffer data; + if (!data.Init(aCx, aChunk)) { + aError.StealExceptionFromJSContext(aCx); + return nullptr; + } + // buffer/bufferView + MOZ_ASSERT(data.IsArrayBuffer() || data.IsArrayBufferView()); + + RefPtr<Promise> promise = Promise::Create(mParent, aError); + if (NS_WARN_IF(aError.Failed())) { + return nullptr; + } + + // Try to write first, and only enqueue data if we were already blocked + // or the write didn't write it all. This avoids allocations and copies + // in common cases. + MOZ_ASSERT(!mPromise); + MOZ_ASSERT(mWritten == 0); + uint32_t written = 0; + ProcessTypedArraysFixed(data, [&](const Span<uint8_t>& aData) { + Span<uint8_t> dataSpan = aData; + nsresult rv = mOutput->Write(mozilla::AsChars(dataSpan).Elements(), + dataSpan.Length(), &written); + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + promise->MaybeRejectWithAbortError("error writing data"); + return; + } + if (NS_SUCCEEDED(rv)) { + if (written == dataSpan.Length()) { + promise->MaybeResolveWithUndefined(); + return; + } + dataSpan = dataSpan.From(written); + } + + auto buffer = Buffer<uint8_t>::CopyFrom(dataSpan); + if (buffer.isNothing()) { + promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + return; + } + mData = std::move(buffer); + }); + + if (promise->State() != Promise::PromiseState::Pending) { + return promise.forget(); + } + + mPromise = promise; + + nsCOMPtr<nsIEventTarget> target = mozilla::GetCurrentSerialEventTarget(); + nsresult rv = mOutput->AsyncWait(this, 0, 0, target); + if (NS_FAILED(rv)) { + ClearData(); + promise->MaybeRejectWithUnknownError("error waiting to write data"); + } + return promise.forget(); +} + +already_AddRefed<Promise> WritableStreamToOutput::AbortCallbackImpl( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + // https://streams.spec.whatwg.org/#writablestream-set-up + // Step 3. Let abortAlgorithmWrapper be an algorithm that runs these steps: + + if (aReason.WasPassed() && aReason.Value().isObject()) { + JS::Rooted<JSObject*> obj(aCx, &aReason.Value().toObject()); + RefPtr<WebTransportError> error; + UnwrapObject<prototypes::id::WebTransportError, WebTransportError>( + obj, error, nullptr); + if (error) { + mOutput->CloseWithStatus(net::GetNSResultFromWebTransportError( + error->GetStreamErrorCode().Value())); + return nullptr; + } + } + + // XXX The close or rather a dedicated abort should be async. For now we have + // to always fall back to the Step 3.3 below. + // XXX how do we know this stream is used by webtransport? + mOutput->CloseWithStatus(NS_ERROR_WEBTRANSPORT_CODE_BASE); + + // Step 3.3. Return a promise resolved with undefined. + // Wrapper handles this + return nullptr; +} + +void WritableStreamToOutput::ReleaseObjects() { mOutput->Close(); } diff --git a/dom/streams/UnderlyingSinkCallbackHelpers.h b/dom/streams/UnderlyingSinkCallbackHelpers.h new file mode 100644 index 0000000000..c99c8709ce --- /dev/null +++ b/dom/streams/UnderlyingSinkCallbackHelpers.h @@ -0,0 +1,202 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_UnderlyingSinkCallbackHelpers_h +#define mozilla_dom_UnderlyingSinkCallbackHelpers_h + +#include "mozilla/Maybe.h" +#include "mozilla/Buffer.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/UnderlyingSinkBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsISupportsImpl.h" +#include "nsIAsyncOutputStream.h" + +/* + * See the comment in UnderlyingSourceCallbackHelpers.h! + * + * A native implementation of these callbacks is however currently not required. + */ +namespace mozilla::dom { + +class WritableStreamDefaultController; + +class UnderlyingSinkAlgorithmsBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(UnderlyingSinkAlgorithmsBase) + + MOZ_CAN_RUN_SCRIPT virtual void StartCallback( + JSContext* aCx, WritableStreamDefaultController& aController, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) = 0; + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) = 0; + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> CloseCallback( + JSContext* aCx, ErrorResult& aRv) = 0; + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) = 0; + + // Implement this when you need to release underlying resources immediately + // from closed/errored(aborted) streams, without waiting for GC. + virtual void ReleaseObjects() {} + + protected: + virtual ~UnderlyingSinkAlgorithmsBase() = default; +}; + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink +class UnderlyingSinkAlgorithms final : public UnderlyingSinkAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + UnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) + + UnderlyingSinkAlgorithms(nsIGlobalObject* aGlobal, + JS::Handle<JSObject*> aUnderlyingSink, + UnderlyingSink& aUnderlyingSinkDict) + : mGlobal(aGlobal), mUnderlyingSink(aUnderlyingSink) { + // Step 6. (implicit Step 2.) + if (aUnderlyingSinkDict.mStart.WasPassed()) { + mStartCallback = aUnderlyingSinkDict.mStart.Value(); + } + + // Step 7. (implicit Step 3.) + if (aUnderlyingSinkDict.mWrite.WasPassed()) { + mWriteCallback = aUnderlyingSinkDict.mWrite.Value(); + } + + // Step 8. (implicit Step 4.) + if (aUnderlyingSinkDict.mClose.WasPassed()) { + mCloseCallback = aUnderlyingSinkDict.mClose.Value(); + } + + // Step 9. (implicit Step 5.) + if (aUnderlyingSinkDict.mAbort.WasPassed()) { + mAbortCallback = aUnderlyingSinkDict.mAbort.Value(); + } + + mozilla::HoldJSObjects(this); + }; + + MOZ_CAN_RUN_SCRIPT void StartCallback( + JSContext* aCx, WritableStreamDefaultController& aController, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseCallback( + JSContext* aCx, ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override; + + protected: + ~UnderlyingSinkAlgorithms() override { mozilla::DropJSObjects(this); } + + private: + // Virtually const, but are cycle collected + nsCOMPtr<nsIGlobalObject> mGlobal; + JS::Heap<JSObject*> mUnderlyingSink; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSinkStartCallback> mStartCallback; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSinkWriteCallback> mWriteCallback; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSinkCloseCallback> mCloseCallback; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSinkAbortCallback> mAbortCallback; +}; + +// https://streams.spec.whatwg.org/#writablestream-set-up +// Wrappers defined by the "Set up" methods in the spec. +// (closeAlgorithmWrapper, abortAlgorithmWrapper) +// This helps you just return nullptr when 1) the algorithm is synchronous, or +// 2) an error occurred, as this wrapper will return a resolved or rejected +// promise respectively. +// Note that StartCallback is only for JS consumers to access the +// controller, and thus is no-op here since native consumers can call +// `ErrorNative()` etc. without direct controller access. +class UnderlyingSinkAlgorithmsWrapper : public UnderlyingSinkAlgorithmsBase { + public: + void StartCallback(JSContext* aCx, + WritableStreamDefaultController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) final { + // Step 1: Let startAlgorithm be an algorithm that returns undefined. + aRetVal.setUndefined(); + } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseCallback( + JSContext* aCx, ErrorResult& aRv) final; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> AbortCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) final; + + virtual already_AddRefed<Promise> CloseCallbackImpl(JSContext* aCx, + ErrorResult& aRv) { + // (closeAlgorithm is optional, give null by default) + return nullptr; + } + + virtual already_AddRefed<Promise> AbortCallbackImpl( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + // (abortAlgorithm is optional, give null by default) + return nullptr; + } +}; + +class WritableStreamToOutput final : public UnderlyingSinkAlgorithmsWrapper, + public nsIOutputStreamCallback { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WritableStreamToOutput, + UnderlyingSinkAlgorithmsBase) + + WritableStreamToOutput(nsIGlobalObject* aParent, + nsIAsyncOutputStream* aOutput) + : mWritten(0), mParent(aParent), mOutput(aOutput) {} + + // Streams algorithms + + already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) override; + + // No CloseCallbackImpl() since ReleaseObjects() will call Close() + + already_AddRefed<Promise> AbortCallbackImpl( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override; + + void ReleaseObjects() override; + + private: + ~WritableStreamToOutput() override = default; + + void ClearData() { + mData = Nothing(); + mPromise = nullptr; + mWritten = 0; + } + + uint32_t mWritten; + nsCOMPtr<nsIGlobalObject> mParent; + nsCOMPtr<nsIAsyncOutputStream> mOutput; + RefPtr<Promise> mPromise; // Resolved when entirely written + Maybe<Buffer<uint8_t>> mData; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/UnderlyingSourceCallbackHelpers.cpp b/dom/streams/UnderlyingSourceCallbackHelpers.cpp new file mode 100644 index 0000000000..8435050214 --- /dev/null +++ b/dom/streams/UnderlyingSourceCallbackHelpers.cpp @@ -0,0 +1,584 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StreamUtils.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/ReadableByteStreamController.h" +#include "mozilla/dom/UnderlyingSourceCallbackHelpers.h" +#include "mozilla/dom/UnderlyingSourceBinding.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "js/experimental/TypedData.h" +#include "nsStreamUtils.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +// UnderlyingSourceAlgorithmsBase +NS_IMPL_CYCLE_COLLECTION(UnderlyingSourceAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTING_ADDREF(UnderlyingSourceAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(UnderlyingSourceAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UnderlyingSourceAlgorithmsBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_INHERITED_WITH_JS_MEMBERS( + UnderlyingSourceAlgorithms, UnderlyingSourceAlgorithmsBase, + (mGlobal, mStartCallback, mPullCallback, mCancelCallback), + (mUnderlyingSource)) +NS_IMPL_ADDREF_INHERITED(UnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_IMPL_RELEASE_INHERITED(UnderlyingSourceAlgorithms, + UnderlyingSourceAlgorithmsBase) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UnderlyingSourceAlgorithms) +NS_INTERFACE_MAP_END_INHERITING(UnderlyingSourceAlgorithmsBase) + +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller-from-underlying-source +void UnderlyingSourceAlgorithms::StartCallback( + JSContext* aCx, ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) { + if (!mStartCallback) { + // Step 2: Let startAlgorithm be an algorithm that returns undefined. + aRetVal.setUndefined(); + return; + } + + // Step 5: If underlyingSourceDict["start"] exists, then set startAlgorithm to + // an algorithm which returns the result of invoking + // underlyingSourceDict["start"] with argument list « controller » and + // callback this value underlyingSource. + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSource); + ReadableStreamDefaultControllerOrReadableByteStreamController controller; + if (aController.IsDefault()) { + controller.SetAsReadableStreamDefaultController() = aController.AsDefault(); + } else { + controller.SetAsReadableByteStreamController() = aController.AsByte(); + } + + return mStartCallback->Call(thisObj, controller, aRetVal, aRv, + "UnderlyingSource.start", + CallbackFunction::eRethrowExceptions); +} + +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller-from-underlying-source +already_AddRefed<Promise> UnderlyingSourceAlgorithms::PullCallback( + JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) { + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSource); + if (!mPullCallback) { + // Step 3: Let pullAlgorithm be an algorithm that returns a promise resolved + // with undefined. + return Promise::CreateResolvedWithUndefined(mGlobal, aRv); + } + + // Step 6: If underlyingSourceDict["pull"] exists, then set pullAlgorithm to + // an algorithm which returns the result of invoking + // underlyingSourceDict["pull"] with argument list « controller » and callback + // this value underlyingSource. + ReadableStreamDefaultControllerOrReadableByteStreamController controller; + if (aController.IsDefault()) { + controller.SetAsReadableStreamDefaultController() = aController.AsDefault(); + } else { + controller.SetAsReadableByteStreamController() = aController.AsByte(); + } + + RefPtr<Promise> promise = + mPullCallback->Call(thisObj, controller, aRv, "UnderlyingSource.pull", + CallbackFunction::eRethrowExceptions); + + return promise.forget(); +} + +// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller-from-underlying-source +already_AddRefed<Promise> UnderlyingSourceAlgorithms::CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + if (!mCancelCallback) { + // Step 4: Let cancelAlgorithm be an algorithm that returns a promise + // resolved with undefined. + return Promise::CreateResolvedWithUndefined(mGlobal, aRv); + } + + // Step 7: If underlyingSourceDict["cancel"] exists, then set cancelAlgorithm + // to an algorithm which takes an argument reason and returns the result of + // invoking underlyingSourceDict["cancel"] with argument list « reason » and + // callback this value underlyingSource. + JS::Rooted<JSObject*> thisObj(aCx, mUnderlyingSource); + RefPtr<Promise> promise = + mCancelCallback->Call(thisObj, aReason, aRv, "UnderlyingSource.cancel", + CallbackFunction::eRethrowExceptions); + + return promise.forget(); +} + +// Shared between: +// https://streams.spec.whatwg.org/#readablestream-set-up +// https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support +// Step 1: Let startAlgorithm be an algorithm that returns undefined. +void UnderlyingSourceAlgorithmsWrapper::StartCallback( + JSContext*, ReadableStreamController&, JS::MutableHandle<JS::Value> aRetVal, + ErrorResult&) { + aRetVal.setUndefined(); +} + +// Shared between: +// https://streams.spec.whatwg.org/#readablestream-set-up +// https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support +// Step 2: Let pullAlgorithmWrapper be an algorithm that runs these steps: +already_AddRefed<Promise> UnderlyingSourceAlgorithmsWrapper::PullCallback( + JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = aController.GetParentObject(); + return PromisifyAlgorithm( + global, + [&](ErrorResult& aRv) MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + return PullCallbackImpl(aCx, aController, aRv); + }, + aRv); +} + +// Shared between: +// https://streams.spec.whatwg.org/#readablestream-set-up +// https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support +// Step 3: Let cancelAlgorithmWrapper be an algorithm that runs these steps: +already_AddRefed<Promise> UnderlyingSourceAlgorithmsWrapper::CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx); + return PromisifyAlgorithm( + global, + [&](ErrorResult& aRv) MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION { + return CancelCallbackImpl(aCx, aReason, aRv); + }, + aRv); +} + +NS_IMPL_ISUPPORTS(InputStreamHolder, nsIInputStreamCallback) + +InputStreamHolder::InputStreamHolder(nsIGlobalObject* aGlobal, + InputToReadableStreamAlgorithms* aCallback, + nsIAsyncInputStream* aInput) + : GlobalTeardownObserver(aGlobal), mCallback(aCallback), mInput(aInput) {} + +void InputStreamHolder::Init(JSContext* aCx) { + if (!NS_IsMainThread()) { + // We're in a worker + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(workerPrivate); + + workerPrivate->AssertIsOnWorkerThread(); + + // Note, this will create a ref-cycle between the holder and the stream. + // The cycle is broken when the stream is closed or the worker begins + // shutting down. + mWorkerRef = StrongWorkerRef::Create(workerPrivate, "InputStreamHolder", + [self = RefPtr{this}]() {}); + if (NS_WARN_IF(!mWorkerRef)) { + return; + } + } +} + +InputStreamHolder::~InputStreamHolder() = default; + +void InputStreamHolder::DisconnectFromOwner() { + Shutdown(); + GlobalTeardownObserver::DisconnectFromOwner(); +} + +void InputStreamHolder::Shutdown() { + if (mInput) { + mInput->Close(); + } + // NOTE(krosylight): Dropping mAsyncWaitAlgorithms here means letting cycle + // collection happen on the underlying source, which can cause a dangling + // read promise that never resolves. Doing so shouldn't be a problem at + // shutdown phase. + // Note that this is currently primarily for Fetch which does not explicitly + // close its streams at shutdown. (i.e. to prevent memory leak for cases e.g + // WPT /fetch/api/basic/stream-response.any.html) + mAsyncWaitAlgorithms = nullptr; + // If we have an AsyncWait running, we'll get a callback and clear + // the mAsyncWaitWorkerRef + mWorkerRef = nullptr; +} + +nsresult InputStreamHolder::AsyncWait(uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + nsresult rv = mInput->AsyncWait(this, aFlags, aRequestedCount, aEventTarget); + if (NS_SUCCEEDED(rv)) { + mAsyncWaitWorkerRef = mWorkerRef; + mAsyncWaitAlgorithms = mCallback; + } + return rv; +} + +NS_IMETHODIMP InputStreamHolder::OnInputStreamReady( + nsIAsyncInputStream* aStream) { + mAsyncWaitWorkerRef = nullptr; + mAsyncWaitAlgorithms = nullptr; + // We may get called back after ::Shutdown() + if (mCallback) { + return mCallback->OnInputStreamReady(aStream); + } + return NS_ERROR_FAILURE; +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(InputToReadableStreamAlgorithms, + UnderlyingSourceAlgorithmsWrapper, + nsIInputStreamCallback) +NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(InputToReadableStreamAlgorithms, + UnderlyingSourceAlgorithmsWrapper, + mPullPromise, mStream) + +InputToReadableStreamAlgorithms::InputToReadableStreamAlgorithms( + JSContext* aCx, nsIAsyncInputStream* aInput, ReadableStream* aStream) + : mOwningEventTarget(GetCurrentSerialEventTarget()), + mInput(new InputStreamHolder(aStream->GetParentObject(), this, aInput)), + mStream(aStream) { + mInput->Init(aCx); +} + +already_AddRefed<Promise> InputToReadableStreamAlgorithms::PullCallbackImpl( + JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) { + MOZ_ASSERT(aController.IsByte()); + ReadableStream* stream = aController.Stream(); + MOZ_ASSERT(stream); + + MOZ_DIAGNOSTIC_ASSERT(stream->Disturbed()); + + MOZ_DIAGNOSTIC_ASSERT(!IsClosed()); + MOZ_ASSERT(!mPullPromise); + mPullPromise = Promise::CreateInfallible(aController.GetParentObject()); + + MOZ_DIAGNOSTIC_ASSERT(mInput); + + nsresult rv = mInput->AsyncWait(0, 0, mOwningEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + ErrorPropagation(aCx, stream, rv); + return nullptr; + } + + // All good. + return do_AddRef(mPullPromise); +} + +// _BOUNDARY because OnInputStreamReady doesn't have [can-run-script] +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +InputToReadableStreamAlgorithms::OnInputStreamReady( + nsIAsyncInputStream* aStream) { + MOZ_DIAGNOSTIC_ASSERT(aStream); + + // Already closed. We have nothing else to do here. + if (IsClosed()) { + return NS_OK; + } + + AutoEntryScript aes(mStream->GetParentObject(), + "InputToReadableStream data available"); + + MOZ_DIAGNOSTIC_ASSERT(mInput); + + JSContext* cx = aes.cx(); + + uint64_t size = 0; + nsresult rv = mInput->Available(&size); + MOZ_ASSERT_IF(NS_SUCCEEDED(rv), size > 0); + + // No warning for stream closed. + if (rv == NS_BASE_STREAM_CLOSED || NS_WARN_IF(NS_FAILED(rv))) { + ErrorPropagation(cx, mStream, rv); + return NS_OK; + } + + // Not having a promise means we are pinged by stream closure + // (WAIT_CLOSURE_ONLY below), but here we still have more data to read. Let's + // wait for the next read request in that case. + if (!mPullPromise) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mPullPromise->State() == + Promise::PromiseState::Pending); + + ErrorResult errorResult; + PullFromInputStream(cx, size, errorResult); + errorResult.WouldReportJSException(); + if (errorResult.Failed()) { + ErrorPropagation(cx, mStream, errorResult.StealNSResult()); + return NS_OK; + } + + // PullFromInputStream can fulfill read request, which can trigger read + // request chunk steps, which again may execute JS. But it should be still + // safe from cycle collection as the caller nsIAsyncInputStream should hold + // the reference of `this`. + // + // That said, it's generally good to be cautious as there's no guarantee that + // the interface is implemented in the safest way. + MOZ_DIAGNOSTIC_ASSERT(mPullPromise); + if (mPullPromise) { + mPullPromise->MaybeResolveWithUndefined(); + mPullPromise = nullptr; + } + + MOZ_DIAGNOSTIC_ASSERT(mInput); + if (mInput) { + // Subscribe WAIT_CLOSURE_ONLY so that OnInputStreamReady can be called when + // mInput is closed. + rv = mInput->AsyncWait(nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, + mOwningEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + ErrorPropagation(cx, mStream, errorResult.StealNSResult()); + return NS_OK; + } + } + + return NS_OK; +} + +void InputToReadableStreamAlgorithms::WriteIntoReadRequestBuffer( + JSContext* aCx, ReadableStream* aStream, JS::Handle<JSObject*> aBuffer, + uint32_t aLength, uint32_t* aByteWritten, ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(aBuffer); + MOZ_DIAGNOSTIC_ASSERT(aByteWritten); + MOZ_DIAGNOSTIC_ASSERT(mInput); + MOZ_DIAGNOSTIC_ASSERT(!IsClosed()); + MOZ_DIAGNOSTIC_ASSERT(mPullPromise->State() == + Promise::PromiseState::Pending); + + uint32_t written; + nsresult rv; + void* buffer; + { + // Bug 1754513: Hazard suppression. + // + // Because mInput->Read is detected as possibly GCing by the + // current state of our static hazard analysis, we need to do the + // suppression here. This can be removed with future improvements + // to the static analysis. + JS::AutoSuppressGCAnalysis suppress; + JS::AutoCheckCannotGC noGC; + bool isSharedMemory; + + buffer = JS_GetArrayBufferViewData(aBuffer, &isSharedMemory, noGC); + MOZ_ASSERT(!isSharedMemory); + + rv = mInput->Read(static_cast<char*>(buffer), aLength, &written); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return; + } + } + + *aByteWritten = written; + + if (written == 0) { + // If bytesWritten is zero, then the stream has been closed; return rather + // than enqueueing a chunk filled with zeros. + aRv.Throw(NS_BASE_STREAM_CLOSED); + return; + } + + // All good. +} + +// https://streams.spec.whatwg.org/#readablestream-pull-from-bytes +// This is a ReadableStream algorithm but will probably be used solely in +// InputToReadableStreamAlgorithms. +void InputToReadableStreamAlgorithms::PullFromInputStream(JSContext* aCx, + uint64_t aAvailable, + ErrorResult& aRv) { + // Step 1. Assert: stream.[[controller]] implements + // ReadableByteStreamController. + MOZ_ASSERT(mStream->Controller()->IsByte()); + + // Step 2. Let available be bytes’s length. (aAvailable) + // Step 3. Let desiredSize be available. + uint64_t desiredSize = aAvailable; + + // Step 4. If stream’s current BYOB request view is non-null, then set + // desiredSize to stream’s current BYOB request view's byte length. + JS::Rooted<JSObject*> byobView(aCx); + mStream->GetCurrentBYOBRequestView(aCx, &byobView, aRv); + if (aRv.Failed()) { + return; + } + if (byobView) { + desiredSize = JS_GetArrayBufferViewByteLength(byobView); + } + + // Step 5. Let pullSize be the smaller value of available and desiredSize. + // + // To avoid OOMing up on huge amounts of available data on a 32 bit system, + // as well as potentially overflowing nsIInputStream's Read method's + // parameter, let's limit our maximum chunk size to 256MB. + // + // (Note that nsIInputStream uses uint64_t for Available and uint32_t for + // Read.) + uint64_t pullSize = std::min(static_cast<uint64_t>(256 * 1024 * 1024), + std::min(aAvailable, desiredSize)); + + // Step 6. Let pulled be the first pullSize bytes of bytes. + // Step 7. Remove the first pullSize bytes from bytes. + // + // We do this in step 8 and 9, as we don't have a direct access to the data + // but need to let nsIInputStream to write into the view. + + // Step 8. If stream’s current BYOB request view is non-null, then: + if (byobView) { + // Step 8.1. Write pulled into stream’s current BYOB request view. + uint32_t bytesWritten = 0; + WriteIntoReadRequestBuffer(aCx, mStream, byobView, pullSize, &bytesWritten, + aRv); + if (aRv.Failed()) { + return; + } + + // Step 8.2. Perform ? + // ReadableByteStreamControllerRespond(stream.[[controller]], pullSize). + // + // But we do not use pullSize but use byteWritten here, since nsIInputStream + // does not guarantee to read as much as it told in Available(). + MOZ_DIAGNOSTIC_ASSERT(pullSize == bytesWritten); + ReadableByteStreamControllerRespond( + aCx, MOZ_KnownLive(mStream->Controller()->AsByte()), bytesWritten, aRv); + } + // Step 9. Otherwise, + else { + // Step 9.1. Set view to the result of creating a Uint8Array from pulled in + // stream’s relevant Realm. + UniquePtr<uint8_t[], JS::FreePolicy> buffer( + static_cast<uint8_t*>(JS_malloc(aCx, pullSize))); + if (!buffer) { + aRv.ThrowTypeError("Out of memory"); + return; + } + + uint32_t bytesWritten = 0; + nsresult rv = mInput->Read((char*)buffer.get(), pullSize, &bytesWritten); + if (!bytesWritten) { + rv = NS_BASE_STREAM_CLOSED; + } + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(pullSize == bytesWritten); + JS::Rooted<JSObject*> view(aCx, nsJSUtils::MoveBufferAsUint8Array( + aCx, bytesWritten, std::move(buffer))); + if (!view) { + JS_ClearPendingException(aCx); + aRv.ThrowTypeError("Out of memory"); + return; + } + + // Step 9.2. Perform ? + // ReadableByteStreamControllerEnqueue(stream.[[controller]], view). + ReadableByteStreamControllerEnqueue( + aCx, MOZ_KnownLive(mStream->Controller()->AsByte()), view, aRv); + } +} + +void InputToReadableStreamAlgorithms::CloseAndReleaseObjects( + JSContext* aCx, ReadableStream* aStream) { + MOZ_DIAGNOSTIC_ASSERT(!IsClosed()); + + ReleaseObjects(); + + if (aStream->State() == ReadableStream::ReaderState::Readable) { + IgnoredErrorResult rv; + aStream->CloseNative(aCx, rv); + NS_WARNING_ASSERTION(!rv.Failed(), "Failed to Close Stream"); + } +} + +void InputToReadableStreamAlgorithms::ReleaseObjects() { + if (mInput) { + mInput->CloseWithStatus(NS_BASE_STREAM_CLOSED); + mInput->Shutdown(); + mInput = nullptr; + } + + // It's okay to leave a potentially unsettled promise as-is as this is only + // used to prevent reentrant to PullCallback. CloseNative() or ErrorNative() + // will settle the read requests for us. + mPullPromise = nullptr; +} + +nsIInputStream* InputToReadableStreamAlgorithms::MaybeGetInputStreamIfUnread() { + MOZ_ASSERT(!mStream->Disturbed(), + "Should be only called on non-disturbed streams"); + return mInput->GetInputStream(); +} + +void InputToReadableStreamAlgorithms::ErrorPropagation(JSContext* aCx, + ReadableStream* aStream, + nsresult aError) { + // Nothing to do. + if (IsClosed()) { + return; + } + + // Let's close the stream. + if (aError == NS_BASE_STREAM_CLOSED) { + CloseAndReleaseObjects(aCx, aStream); + return; + } + + // Let's use a generic error. + ErrorResult rv; + // XXXbz can we come up with a better error message here to tell the + // consumer what went wrong? + rv.ThrowTypeError("Error in input stream"); + + JS::Rooted<JS::Value> errorValue(aCx); + bool ok = ToJSValue(aCx, std::move(rv), &errorValue); + MOZ_RELEASE_ASSERT(ok, "ToJSValue never fails for ErrorResult"); + + { + // This will be ignored if it's already errored. + IgnoredErrorResult rv; + aStream->ErrorNative(aCx, errorValue, rv); + NS_WARNING_ASSERTION(!rv.Failed(), "Failed to error InputToReadableStream"); + } + + MOZ_ASSERT(IsClosed()); +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + NonAsyncInputToReadableStreamAlgorithms, UnderlyingSourceAlgorithmsWrapper) +NS_IMPL_CYCLE_COLLECTION_INHERITED(NonAsyncInputToReadableStreamAlgorithms, + UnderlyingSourceAlgorithmsWrapper, + mAsyncAlgorithms) + +already_AddRefed<Promise> +NonAsyncInputToReadableStreamAlgorithms::PullCallbackImpl( + JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) { + if (!mAsyncAlgorithms) { + nsCOMPtr<nsIAsyncInputStream> asyncStream; + + // NS_MakeAsyncNonBlockingInputStream may immediately start a stream read + // via nsInputStreamTransport::OpenInputStream, which is why this should be + // called on a pull callback instead of in the constructor. + nsresult rv = NS_MakeAsyncNonBlockingInputStream( + mInput.forget(), getter_AddRefs(asyncStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + + mAsyncAlgorithms = MakeRefPtr<InputToReadableStreamAlgorithms>( + aCx, asyncStream, aController.Stream()); + } + + MOZ_ASSERT(!mInput); + return mAsyncAlgorithms->PullCallbackImpl(aCx, aController, aRv); +} + +} // namespace mozilla::dom diff --git a/dom/streams/UnderlyingSourceCallbackHelpers.h b/dom/streams/UnderlyingSourceCallbackHelpers.h new file mode 100644 index 0000000000..e35e1dc59f --- /dev/null +++ b/dom/streams/UnderlyingSourceCallbackHelpers.h @@ -0,0 +1,330 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_UnderlyingSourceCallbackHelpers_h +#define mozilla_dom_UnderlyingSourceCallbackHelpers_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/UnderlyingSourceBinding.h" +#include "mozilla/WeakPtr.h" +#include "nsIAsyncInputStream.h" +#include "nsISupports.h" +#include "nsISupportsImpl.h" + +/* Since the streams specification has native descriptions of some callbacks + * (i.e. described in prose, rather than provided by user code), we need to be + * able to pass around native callbacks. To handle this, we define polymorphic + * classes That cover the difference between native callback and user-provided. + * + * The Streams specification wants us to invoke these callbacks, run through + * WebIDL as if they were methods. So we have to preserve the underlying object + * to use as the This value on invocation. + */ +enum class nsresult : uint32_t; + +namespace mozilla::dom { + +class StrongWorkerRef; +class BodyStreamHolder; +class ReadableStreamController; +class ReadableStream; + +class UnderlyingSourceAlgorithmsBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(UnderlyingSourceAlgorithmsBase) + + MOZ_CAN_RUN_SCRIPT virtual void StartCallback( + JSContext* aCx, ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv) = 0; + + // A promise-returning algorithm that pulls data from the underlying byte + // source + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> PullCallback( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) = 0; + + // A promise-returning algorithm, taking one argument (the cancel reason), + // which communicates a requested cancelation to the underlying byte source + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) = 0; + + // Implement this when you need to release underlying resources immediately + // from closed(canceled)/errored streams, without waiting for GC. + virtual void ReleaseObjects() {} + + // Can be used to read chunks directly via nsIInputStream to skip JS-related + // overhead, if this readable stream is a wrapper of a native stream. + // Currently used by Fetch helper functions e.g. new Response(stream).text() + virtual nsIInputStream* MaybeGetInputStreamIfUnread() { return nullptr; } + + // https://streams.spec.whatwg.org/#other-specs-rs-create + // By "native" we mean "instances initialized via the above set up or set up + // with byte reading support algorithms (not, e.g., on web-developer-created + // instances)" + virtual bool IsNative() { return true; } + + protected: + virtual ~UnderlyingSourceAlgorithmsBase() = default; +}; + +class UnderlyingSourceAlgorithms final : public UnderlyingSourceAlgorithmsBase { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED( + UnderlyingSourceAlgorithms, UnderlyingSourceAlgorithmsBase) + + UnderlyingSourceAlgorithms(nsIGlobalObject* aGlobal, + JS::Handle<JSObject*> aUnderlyingSource, + UnderlyingSource& aUnderlyingSourceDict) + : mGlobal(aGlobal), mUnderlyingSource(aUnderlyingSource) { + // Step 6. (implicit Step 2.) + if (aUnderlyingSourceDict.mStart.WasPassed()) { + mStartCallback = aUnderlyingSourceDict.mStart.Value(); + } + + // Step 7. (implicit Step 3.) + if (aUnderlyingSourceDict.mPull.WasPassed()) { + mPullCallback = aUnderlyingSourceDict.mPull.Value(); + } + + // Step 8. (implicit Step 4.) + if (aUnderlyingSourceDict.mCancel.WasPassed()) { + mCancelCallback = aUnderlyingSourceDict.mCancel.Value(); + } + + mozilla::HoldJSObjects(this); + }; + + MOZ_CAN_RUN_SCRIPT void StartCallback(JSContext* aCx, + ReadableStreamController& aController, + JS::MutableHandle<JS::Value> aRetVal, + ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PullCallback( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) override; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override; + + bool IsNative() override { return false; } + + protected: + ~UnderlyingSourceAlgorithms() override { mozilla::DropJSObjects(this); }; + + private: + // Virtually const, but are cycle collected + nsCOMPtr<nsIGlobalObject> mGlobal; + JS::Heap<JSObject*> mUnderlyingSource; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSourceStartCallback> mStartCallback; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSourcePullCallback> mPullCallback; + MOZ_KNOWN_LIVE RefPtr<UnderlyingSourceCancelCallback> mCancelCallback; +}; + +// https://streams.spec.whatwg.org/#readablestream-set-up +// https://streams.spec.whatwg.org/#readablestream-set-up-with-byte-reading-support +// Wrappers defined by the "Set up" methods in the spec. This helps you just +// return nullptr when an error occurred as this wrapper converts it to a +// rejected promise. +// Note that StartCallback is only for JS consumers to access +// the controller, and thus is no-op here since native consumers can call +// `EnqueueNative()` etc. without direct controller access. +class UnderlyingSourceAlgorithmsWrapper + : public UnderlyingSourceAlgorithmsBase { + void StartCallback(JSContext*, ReadableStreamController&, + JS::MutableHandle<JS::Value> aRetVal, ErrorResult&) final; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> PullCallback( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) final; + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CancelCallback( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) final; + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> PullCallbackImpl( + JSContext* aCx, ReadableStreamController& aController, ErrorResult& aRv) { + // pullAlgorithm is optional, return null by default + return nullptr; + } + + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> CancelCallbackImpl( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) { + // cancelAlgorithm is optional, return null by default + return nullptr; + } +}; + +class InputToReadableStreamAlgorithms; + +// This class exists to isolate InputToReadableStreamAlgorithms from the +// nsIAsyncInputStream. If we call AsyncWait(this,...), it holds a +// reference to 'this' which can't be cc'd, and we can leak the stream, +// causing a Worker to assert with globalScopeAlive. By isolating +// ourselves from the inputstream, we can safely be CC'd if needed and +// will inform the inputstream to shut down. +class InputStreamHolder final : public nsIInputStreamCallback, + public GlobalTeardownObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAMCALLBACK + + InputStreamHolder(nsIGlobalObject* aGlobal, + InputToReadableStreamAlgorithms* aCallback, + nsIAsyncInputStream* aInput); + + void Init(JSContext* aCx); + + void DisconnectFromOwner() override; + + // Used by global teardown + void Shutdown(); + + // These just proxy the calls to the nsIAsyncInputStream + nsresult AsyncWait(uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget); + nsresult Available(uint64_t* aSize) { return mInput->Available(aSize); } + nsresult Read(char* aBuffer, uint32_t aLength, uint32_t* aWritten) { + return mInput->Read(aBuffer, aLength, aWritten); + } + nsresult CloseWithStatus(nsresult aStatus) { + return mInput->CloseWithStatus(aStatus); + } + + nsIAsyncInputStream* GetInputStream() { return mInput; } + + private: + ~InputStreamHolder(); + + // WeakPtr to avoid cycles + WeakPtr<InputToReadableStreamAlgorithms> mCallback; + // To ensure the worker sticks around + RefPtr<StrongWorkerRef> mAsyncWaitWorkerRef; + RefPtr<StrongWorkerRef> mWorkerRef; + nsCOMPtr<nsIAsyncInputStream> mInput; + + // To ensure the underlying source sticks around during an ongoing read + // operation. mAlgorithms is not cycle collected on purpose, and this holder + // is responsible to keep the underlying source algorithms until + // nsIAsyncInputStream responds. + // + // This is done because otherwise the whole stream objects may be cycle + // collected, including the promises created from read(), as our JS engine may + // throw unsettled promises away for optimization. See bug 1849860. + RefPtr<InputToReadableStreamAlgorithms> mAsyncWaitAlgorithms; +}; + +// Using this class means you are also passing the lifetime control of your +// nsIAsyncInputStream, as it will be closed when this class tears down. +class InputToReadableStreamAlgorithms final + : public UnderlyingSourceAlgorithmsWrapper, + public nsIInputStreamCallback, + public SupportsWeakPtr { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InputToReadableStreamAlgorithms, + UnderlyingSourceAlgorithmsWrapper) + + InputToReadableStreamAlgorithms(JSContext* aCx, nsIAsyncInputStream* aInput, + ReadableStream* aStream); + + // Streams algorithms + + already_AddRefed<Promise> PullCallbackImpl( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) override; + + void ReleaseObjects() override; + + nsIInputStream* MaybeGetInputStreamIfUnread() override; + + private: + ~InputToReadableStreamAlgorithms() { + if (mInput) { + mInput->Shutdown(); + } + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void CloseAndReleaseObjects( + JSContext* aCx, ReadableStream* aStream); + + void WriteIntoReadRequestBuffer(JSContext* aCx, ReadableStream* aStream, + JS::Handle<JSObject*> aBuffer, + uint32_t aLength, uint32_t* aByteWritten, + ErrorResult& aRv); + + // https://streams.spec.whatwg.org/#readablestream-pull-from-bytes + // (Uses InputStreamHolder for the "byte sequence" in the spec) + MOZ_CAN_RUN_SCRIPT void PullFromInputStream(JSContext* aCx, + uint64_t aAvailable, + ErrorResult& aRv); + + void ErrorPropagation(JSContext* aCx, ReadableStream* aStream, + nsresult aError); + + // Common methods + + bool IsClosed() { return !mInput; } + + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + + // This promise is created by PullCallback and resolved when + // OnInputStreamReady succeeds. No need to try hard to settle it though, see + // also ReleaseObjects() for the reason. + RefPtr<Promise> mPullPromise; + + RefPtr<InputStreamHolder> mInput; + + // mStream never changes after construction and before CC + MOZ_KNOWN_LIVE RefPtr<ReadableStream> mStream; +}; + +class NonAsyncInputToReadableStreamAlgorithms + : public UnderlyingSourceAlgorithmsWrapper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + NonAsyncInputToReadableStreamAlgorithms, + UnderlyingSourceAlgorithmsWrapper) + + explicit NonAsyncInputToReadableStreamAlgorithms(nsIInputStream& aInput) + : mInput(&aInput) {} + + already_AddRefed<Promise> PullCallbackImpl( + JSContext* aCx, ReadableStreamController& aController, + ErrorResult& aRv) override; + + void ReleaseObjects() override { + if (RefPtr<InputToReadableStreamAlgorithms> algorithms = + mAsyncAlgorithms.forget()) { + algorithms->ReleaseObjects(); + } + if (nsCOMPtr<nsIInputStream> input = mInput.forget()) { + input->Close(); + } + } + + nsIInputStream* MaybeGetInputStreamIfUnread() override { + MOZ_ASSERT(mInput, "Should be only called on non-disturbed streams"); + return mInput; + } + + private: + ~NonAsyncInputToReadableStreamAlgorithms() = default; + + nsCOMPtr<nsIInputStream> mInput; + RefPtr<InputToReadableStreamAlgorithms> mAsyncAlgorithms; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/streams/WritableStream.cpp b/dom/streams/WritableStream.cpp new file mode 100644 index 0000000000..8dab9a564e --- /dev/null +++ b/dom/streams/WritableStream.cpp @@ -0,0 +1,811 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WritableStream.h" + +#include "StreamUtils.h" +#include "js/Array.h" +#include "js/PropertyAndElement.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/BindingCallContext.h" +#include "mozilla/dom/QueueWithSizes.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/ReadRequest.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/UnderlyingSinkBinding.h" +#include "mozilla/dom/WritableStreamBinding.h" +#include "mozilla/dom/WritableStreamDefaultController.h" +#include "mozilla/dom/WritableStreamDefaultWriter.h" +#include "nsCOMPtr.h" + +#include "mozilla/dom/Promise-inl.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WITH_JS_MEMBERS( + WritableStream, + (mGlobal, mCloseRequest, mController, mInFlightWriteRequest, + mInFlightCloseRequest, mPendingAbortRequestPromise, mWriter, + mWriteRequests), + (mPendingAbortRequestReason, mStoredError)) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WritableStream) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(WritableStream, + LastRelease()) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WritableStream) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +WritableStream::WritableStream(nsIGlobalObject* aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller) + : mGlobal(aGlobal), mHoldDropCaller(aHoldDropCaller) { + if (mHoldDropCaller == HoldDropJSObjectsCaller::Implicit) { + mozilla::HoldJSObjects(this); + } +} + +WritableStream::WritableStream(const GlobalObject& aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller) + : mGlobal(do_QueryInterface(aGlobal.GetAsSupports())), + mHoldDropCaller(aHoldDropCaller) { + if (mHoldDropCaller == HoldDropJSObjectsCaller::Implicit) { + mozilla::HoldJSObjects(this); + } +} + +WritableStream::~WritableStream() { + if (mHoldDropCaller == HoldDropJSObjectsCaller::Implicit) { + mozilla::DropJSObjects(this); + } +} + +JSObject* WritableStream::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return WritableStream_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://streams.spec.whatwg.org/#writable-stream-deal-with-rejection +void WritableStream::DealWithRejection(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1. Let state be stream.[[state]]. + // Step 2. If state is "writable", + if (mState == WriterState::Writable) { + // Step 2.1. Perform ! WritableStreamStartErroring(stream, error). + StartErroring(aCx, aError, aRv); + + // Step 2.2. Return. + return; + } + + // Step 3. Assert: state is "erroring". + MOZ_ASSERT(mState == WriterState::Erroring); + + // Step 4. Perform ! WritableStreamFinishErroring(stream). + FinishErroring(aCx, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-finish-erroring +void WritableStream::FinishErroring(JSContext* aCx, ErrorResult& aRv) { + // Step 1. Assert: stream.[[state]] is "erroring". + MOZ_ASSERT(mState == WriterState::Erroring); + + // Step 2. Assert: ! WritableStreamHasOperationMarkedInFlight(stream) is + // false. + MOZ_ASSERT(!HasOperationMarkedInFlight()); + + // Step 3. Set stream.[[state]] to "errored". + mState = WriterState::Errored; + + // Step 4. Perform ! stream.[[controller]].[[ErrorSteps]](). + Controller()->ErrorSteps(); + + // Step 5. Let storedError be stream.[[storedError]]. + JS::Rooted<JS::Value> storedError(aCx, mStoredError); + + // Step 6. For each writeRequest of stream.[[writeRequests]]: + for (const RefPtr<Promise>& writeRequest : mWriteRequests) { + // Step 6.1. Reject writeRequest with storedError. + writeRequest->MaybeReject(storedError); + } + + // Step 7. Set stream.[[writeRequests]] to an empty list. + mWriteRequests.Clear(); + + // Step 8. If stream.[[pendingAbortRequest]] is undefined, + if (!mPendingAbortRequestPromise) { + // Step 8.1. Perform ! + // WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + RejectCloseAndClosedPromiseIfNeeded(); + + // Step 8.2. Return. + return; + } + + // Step 9. Let abortRequest be stream.[[pendingAbortRequest]]. + RefPtr<Promise> abortPromise = mPendingAbortRequestPromise; + JS::Rooted<JS::Value> abortReason(aCx, mPendingAbortRequestReason); + bool abortWasAlreadyErroring = mPendingAbortRequestWasAlreadyErroring; + + // Step 10. Set stream.[[pendingAbortRequest]] to undefined. + SetPendingAbortRequest(nullptr, JS::UndefinedHandleValue, false); + + // Step 11. If abortRequest’s was already erroring is true, + if (abortWasAlreadyErroring) { + // Step 11.1. Reject abortRequest’s promise with storedError. + abortPromise->MaybeReject(storedError); + + // Step 11.2. Perform ! + // WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + RejectCloseAndClosedPromiseIfNeeded(); + + // Step 11.3. Return. + return; + } + + // Step 12. Let promise be ! + // stream.[[controller]].[[AbortSteps]](abortRequest’s reason). + RefPtr<WritableStreamDefaultController> controller = mController; + RefPtr<Promise> promise = controller->AbortSteps(aCx, abortReason, aRv); + if (aRv.Failed()) { + return; + } + + // Step 13 + 14. + promise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + Promise* aAbortRequestPromise, WritableStream* aStream) { + // Step 13. Upon fulfillment of promise, + // Step 13.1. Resolve abortRequest’s promise with undefined. + aAbortRequestPromise->MaybeResolveWithUndefined(); + + // Step 13.2. Perform ! + // WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + aStream->RejectCloseAndClosedPromiseIfNeeded(); + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + Promise* aAbortRequestPromise, WritableStream* aStream) { + // Step 14. Upon rejection of promise with reason reason, + // Step 14.1. Reject abortRequest’s promise with reason. + aAbortRequestPromise->MaybeReject(aValue); + + // Step 14.2. Perform ! + // WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + aStream->RejectCloseAndClosedPromiseIfNeeded(); + }, + RefPtr(abortPromise), RefPtr(this)); +} + +// https://streams.spec.whatwg.org/#writable-stream-finish-in-flight-close +void WritableStream::FinishInFlightClose() { + // Step 1. Assert: stream.[[inFlightCloseRequest]] is not undefined. + MOZ_ASSERT(mInFlightCloseRequest); + + // Step 2. Resolve stream.[[inFlightCloseRequest]] with undefined. + mInFlightCloseRequest->MaybeResolveWithUndefined(); + + // Step 3. Set stream.[[inFlightCloseRequest]] to undefined. + mInFlightCloseRequest = nullptr; + + // Step 4. Let state be stream.[[state]]. + // Step 5. Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(mState == WriterState::Writable || + mState == WriterState::Erroring); + + // Step 6. If state is "erroring", + if (mState == WriterState::Erroring) { + // Step 6.1. Set stream.[[storedError]] to undefined. + mStoredError.setUndefined(); + + // Step 6.2. If stream.[[pendingAbortRequest]] is not undefined, + if (mPendingAbortRequestPromise) { + // Step 6.2.1. Resolve stream.[[pendingAbortRequest]]'s promise with + // undefined. + mPendingAbortRequestPromise->MaybeResolveWithUndefined(); + + // Step 6.2.2. Set stream.[[pendingAbortRequest]] to undefined. + SetPendingAbortRequest(nullptr, JS::UndefinedHandleValue, false); + } + } + + // Step 7. Set stream.[[state]] to "closed". + mState = WriterState::Closed; + + // Step 8. Let writer be stream.[[writer]]. + // Step 9. If writer is not undefined, resolve writer.[[closedPromise]] with + // undefined. + if (mWriter) { + mWriter->ClosedPromise()->MaybeResolveWithUndefined(); + } + + // Step 10. Assert: stream.[[pendingAbortRequest]] is undefined. + MOZ_ASSERT(!mPendingAbortRequestPromise); + // Assert: stream.[[storedError]] is undefined. + MOZ_ASSERT(mStoredError.isUndefined()); +} + +// https://streams.spec.whatwg.org/#writable-stream-finish-in-flight-close-with-error +void WritableStream::FinishInFlightCloseWithError(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1. Assert: stream.[[inFlightCloseRequest]] is not undefined. + MOZ_ASSERT(mInFlightCloseRequest); + + // Step 2. Reject stream.[[inFlightCloseRequest]] with error. + mInFlightCloseRequest->MaybeReject(aError); + + // Step 3. Set stream.[[inFlightCloseRequest]] to undefined. + mInFlightCloseRequest = nullptr; + + // Step 4. Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(mState == WriterState::Writable || + mState == WriterState::Erroring); + + // Step 5. If stream.[[pendingAbortRequest]] is not undefined, + if (mPendingAbortRequestPromise) { + // Step 5.1. Reject stream.[[pendingAbortRequest]]'s promise with error. + mPendingAbortRequestPromise->MaybeReject(aError); + + // Step 5.2. Set stream.[[pendingAbortRequest]] to undefined. + SetPendingAbortRequest(nullptr, JS::UndefinedHandleValue, false); + } + + // Step 6. Perform ! WritableStreamDealWithRejection(stream, error). + DealWithRejection(aCx, aError, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-finish-in-flight-write +void WritableStream::FinishInFlightWrite() { + // Step 1. Assert: stream.[[inFlightWriteRequest]] is not undefined. + MOZ_ASSERT(mInFlightWriteRequest); + + // Step 2. Resolve stream.[[inFlightWriteRequest]] with undefined. + mInFlightWriteRequest->MaybeResolveWithUndefined(); + + // Step 3. Set stream.[[inFlightWriteRequest]] to undefined. + mInFlightWriteRequest = nullptr; +} + +// https://streams.spec.whatwg.org/#writable-stream-finish-in-flight-write-with-error +void WritableStream::FinishInFlightWriteWithError(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1. Assert: stream.[[inFlightWriteRequest]] is not undefined. + MOZ_ASSERT(mInFlightWriteRequest); + + // Step 2. Reject stream.[[inFlightWriteRequest]] with error. + mInFlightWriteRequest->MaybeReject(aError); + + // Step 3. Set stream.[[inFlightWriteRequest]] to undefined. + mInFlightWriteRequest = nullptr; + + // Step 4. Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(mState == WriterState::Writable || + mState == WriterState::Erroring); + + // Step 5. Perform ! WritableStreamDealWithRejection(stream, error). + DealWithRejection(aCx, aError, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-mark-close-request-in-flight +void WritableStream::MarkCloseRequestInFlight() { + // Step 1. Assert: stream.[[inFlightCloseRequest]] is undefined. + MOZ_ASSERT(!mInFlightCloseRequest); + + // Step 2. Assert: stream.[[closeRequest]] is not undefined. + MOZ_ASSERT(mCloseRequest); + + // Step 3. Set stream.[[inFlightCloseRequest]] to stream.[[closeRequest]]. + mInFlightCloseRequest = mCloseRequest; + + // Step 4. Set stream.[[closeRequest]] to undefined. + mCloseRequest = nullptr; +} + +// https://streams.spec.whatwg.org/#writable-stream-mark-first-write-request-in-flight +void WritableStream::MarkFirstWriteRequestInFlight() { + // Step 1. Assert: stream.[[inFlightWriteRequest]] is undefined. + MOZ_ASSERT(!mInFlightWriteRequest); + + // Step 2. Assert: stream.[[writeRequests]] is not empty. + MOZ_ASSERT(!mWriteRequests.IsEmpty()); + + // Step 3. Let writeRequest be stream.[[writeRequests]][0]. + RefPtr<Promise> writeRequest = mWriteRequests.ElementAt(0); + + // Step 4. Remove writeRequest from stream.[[writeRequests]]. + mWriteRequests.RemoveElementAt(0); + + // Step 5. Set stream.[[inFlightWriteRequest]] to writeRequest. + mInFlightWriteRequest = writeRequest; +} + +// https://streams.spec.whatwg.org/#writable-stream-reject-close-and-closed-promise-if-needed +void WritableStream::RejectCloseAndClosedPromiseIfNeeded() { + // Step 1. Assert: stream.[[state]] is "errored". + MOZ_ASSERT(mState == WriterState::Errored); + + JS::Rooted<JS::Value> storedError(RootingCx(), mStoredError); + // Step 2. If stream.[[closeRequest]] is not undefined, + if (mCloseRequest) { + // Step 2.1. Assert: stream.[[inFlightCloseRequest]] is undefined. + MOZ_ASSERT(!mInFlightCloseRequest); + + // Step 2.2. Reject stream.[[closeRequest]] with stream.[[storedError]]. + mCloseRequest->MaybeReject(storedError); + + // Step 2.3. Set stream.[[closeRequest]] to undefined. + mCloseRequest = nullptr; + } + + // Step 3. Let writer be stream.[[writer]]. + RefPtr<WritableStreamDefaultWriter> writer = mWriter; + + // Step 4. If writer is not undefined, + if (writer) { + // Step 4.1. Reject writer.[[closedPromise]] with stream.[[storedError]]. + RefPtr<Promise> closedPromise = writer->ClosedPromise(); + closedPromise->MaybeReject(storedError); + + // Step 4.2. Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + closedPromise->SetSettledPromiseIsHandled(); + } +} + +// https://streams.spec.whatwg.org/#writable-stream-start-erroring +void WritableStream::StartErroring(JSContext* aCx, + JS::Handle<JS::Value> aReason, + ErrorResult& aRv) { + // Step 1. Assert: stream.[[storedError]] is undefined. + MOZ_ASSERT(mStoredError.isUndefined()); + + // Step 2. Assert: stream.[[state]] is "writable". + MOZ_ASSERT(mState == WriterState::Writable); + + // Step 3. Let controller be stream.[[controller]]. + RefPtr<WritableStreamDefaultController> controller = mController; + // Step 4. Assert: controller is not undefined. + MOZ_ASSERT(controller); + + // Step 5. Set stream.[[state]] to "erroring". + mState = WriterState::Erroring; + + // Step 6. Set stream.[[storedError]] to reason. + mStoredError = aReason; + + // Step 7. Let writer be stream.[[writer]]. + RefPtr<WritableStreamDefaultWriter> writer = mWriter; + // Step 8. If writer is not undefined, perform ! + // WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason). + if (writer) { + WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, aReason); + } + + // Step 9. If ! WritableStreamHasOperationMarkedInFlight(stream) is false + // and controller.[[started]] is true, + // perform !WritableStreamFinishErroring(stream). + if (!HasOperationMarkedInFlight() && controller->Started()) { + FinishErroring(aCx, aRv); + } +} + +// https://streams.spec.whatwg.org/#writable-stream-update-backpressure +void WritableStream::UpdateBackpressure(bool aBackpressure) { + // Step 1. Assert: stream.[[state]] is "writable". + MOZ_ASSERT(mState == WriterState::Writable); + // Step 2. Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false. + MOZ_ASSERT(!CloseQueuedOrInFlight()); + + // Step 3. Let writer be stream.[[writer]]. + RefPtr<WritableStreamDefaultWriter> writer = mWriter; + + // Step 4. If writer is not undefined and backpressure is not + // stream.[[backpressure]], + if (writer && aBackpressure != mBackpressure) { + // Step 4.1. If backpressure is true, set writer.[[readyPromise]] to a new + // promise. + if (aBackpressure) { + RefPtr<Promise> promise = + Promise::CreateInfallible(writer->GetParentObject()); + writer->SetReadyPromise(promise); + } else { + // Step 4.2. Otherwise, + // Step 4.2.1. Assert: backpressure is false. + // Step 4.2.2. Resolve writer.[[readyPromise]] with undefined. + writer->ReadyPromise()->MaybeResolveWithUndefined(); + } + } + + // Step 5. Set stream.[[backpressure]] to backpressure. + mBackpressure = aBackpressure; +} + +// https://streams.spec.whatwg.org/#ws-constructor +already_AddRefed<WritableStream> WritableStream::Constructor( + const GlobalObject& aGlobal, + const Optional<JS::Handle<JSObject*>>& aUnderlyingSink, + const QueuingStrategy& aStrategy, ErrorResult& aRv) { + // Step 1. If underlyingSink is missing, set it to null. + JS::Rooted<JSObject*> underlyingSinkObj( + aGlobal.Context(), + aUnderlyingSink.WasPassed() ? aUnderlyingSink.Value() : nullptr); + + // Step 2. Let underlyingSinkDict be underlyingSink, converted to + // an IDL value of type UnderlyingSink. + RootedDictionary<UnderlyingSink> underlyingSinkDict(aGlobal.Context()); + if (underlyingSinkObj) { + JS::Rooted<JS::Value> objValue(aGlobal.Context(), + JS::ObjectValue(*underlyingSinkObj)); + dom::BindingCallContext callCx(aGlobal.Context(), + "WritableStream.constructor"); + aRv.MightThrowJSException(); + if (!underlyingSinkDict.Init(callCx, objValue)) { + aRv.StealExceptionFromJSContext(aGlobal.Context()); + return nullptr; + } + } + + // Step 3. If underlyingSinkDict["type"] exists, throw a RangeError exception. + if (!underlyingSinkDict.mType.isUndefined()) { + aRv.ThrowRangeError("Implementation preserved member 'type'"); + return nullptr; + } + + // Step 4. Perform ! InitializeWritableStream(this). + RefPtr<WritableStream> writableStream = + new WritableStream(aGlobal, HoldDropJSObjectsCaller::Implicit); + + // Step 5. Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy). + // + // Implementation Note: The specification demands that if the size doesn't + // exist, we instead would provide an algorithm that returns 1. Instead, we + // will teach callers that a missing callback should simply return 1, rather + // than gin up a fake callback here. + // + // This decision may need to be revisited if the default action ever diverges + // within the specification. + RefPtr<QueuingStrategySize> sizeAlgorithm = + aStrategy.mSize.WasPassed() ? &aStrategy.mSize.Value() : nullptr; + + // Step 6. Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). + double highWaterMark = ExtractHighWaterMark(aStrategy, 1, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 7. Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink( + // this, underlyingSink, underlyingSinkDict, highWaterMark, sizeAlgorithm). + SetUpWritableStreamDefaultControllerFromUnderlyingSink( + aGlobal.Context(), writableStream, underlyingSinkObj, underlyingSinkDict, + highWaterMark, sizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return writableStream.forget(); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#writable-stream-abort +already_AddRefed<Promise> WritableStreamAbort(JSContext* aCx, + WritableStream* aStream, + JS::Handle<JS::Value> aReason, + ErrorResult& aRv) { + // Step 1. If stream.[[state]] is "closed" or "errored", return a promise + // resolved with undefined. + if (aStream->State() == WritableStream::WriterState::Closed || + aStream->State() == WritableStream::WriterState::Errored) { + RefPtr<Promise> promise = + Promise::CreateInfallible(aStream->GetParentObject()); + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // Step 2. Signal abort on stream.[[controller]].[[signal]] with reason. + RefPtr<WritableStreamDefaultController> controller = aStream->Controller(); + controller->Signal()->SignalAbort(aReason); + + // Step 3. Let state be stream.[[state]]. + WritableStream::WriterState state = aStream->State(); + + // Step 4. If state is "closed" or "errored", return a promise resolved with + // undefined. Note: We re-check the state because signaling abort runs author + // code and that might have changed the state. + if (aStream->State() == WritableStream::WriterState::Closed || + aStream->State() == WritableStream::WriterState::Errored) { + RefPtr<Promise> promise = + Promise::CreateInfallible(aStream->GetParentObject()); + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // Step 5. If stream.[[pendingAbortRequest]] is not undefined, return + // stream.[[pendingAbortRequest]]'s promise. + if (aStream->GetPendingAbortRequestPromise()) { + RefPtr<Promise> promise = aStream->GetPendingAbortRequestPromise(); + return promise.forget(); + } + + // Step 6. Assert: state is "writable" or "erroring". + MOZ_ASSERT(state == WritableStream::WriterState::Writable || + state == WritableStream::WriterState::Erroring); + + // Step 7. Let wasAlreadyErroring be false. + bool wasAlreadyErroring = false; + + // Step 8. If state is "erroring", + JS::Rooted<JS::Value> reason(aCx, aReason); + if (state == WritableStream::WriterState::Erroring) { + // Step 8.1. Set wasAlreadyErroring to true. + wasAlreadyErroring = true; + // Step 8.2. Set reason to undefined. + reason.setUndefined(); + } + + // Step 9. Let promise be a new promise. + RefPtr<Promise> promise = + Promise::CreateInfallible(aStream->GetParentObject()); + + // Step 10. Set stream.[[pendingAbortRequest]] to a new pending abort request + // whose promise is promise, reason is reason, and was already erroring is + // wasAlreadyErroring. + aStream->SetPendingAbortRequest(promise, reason, wasAlreadyErroring); + + // Step 11. If wasAlreadyErroring is false, perform ! + // WritableStreamStartErroring(stream, reason). + if (!wasAlreadyErroring) { + aStream->StartErroring(aCx, reason, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + // Step 12. Return promise. + return promise.forget(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#ws-abort +already_AddRefed<Promise> WritableStream::Abort(JSContext* aCx, + JS::Handle<JS::Value> aReason, + ErrorResult& aRv) { + // Step 1. If ! IsWritableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (Locked()) { + return Promise::CreateRejectedWithTypeError( + GetParentObject(), "Canceled Locked Stream"_ns, aRv); + } + + // Step 2. Return ! WritableStreamAbort(this, reason). + RefPtr<WritableStream> thisRefPtr = this; + return WritableStreamAbort(aCx, thisRefPtr, aReason, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#writable-stream-close +already_AddRefed<Promise> WritableStreamClose(JSContext* aCx, + WritableStream* aStream, + ErrorResult& aRv) { + // Step 1. Let state be stream.[[state]]. + WritableStream::WriterState state = aStream->State(); + + // Step 2. If state is "closed" or "errored", return a promise rejected with a + // TypeError exception. + if (state == WritableStream::WriterState::Closed || + state == WritableStream::WriterState::Errored) { + return Promise::CreateRejectedWithTypeError( + aStream->GetParentObject(), + "Can not close stream after closing or error"_ns, aRv); + } + + // Step 3. Assert: state is "writable" or "erroring". + MOZ_ASSERT(state == WritableStream::WriterState::Writable || + state == WritableStream::WriterState::Erroring); + + // Step 4. Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false. + MOZ_ASSERT(!aStream->CloseQueuedOrInFlight()); + + // Step 5. Let promise be a new promise. + RefPtr<Promise> promise = + Promise::CreateInfallible(aStream->GetParentObject()); + + // Step 6. Set stream.[[closeRequest]] to promise. + aStream->SetCloseRequest(promise); + + // Step 7. Let writer be stream.[[writer]]. + RefPtr<WritableStreamDefaultWriter> writer = aStream->GetWriter(); + + // Step 8. If writer is not undefined, and stream.[[backpressure]] is true, + // and state is "writable", resolve writer.[[readyPromise]] with undefined. + if (writer && aStream->Backpressure() && + state == WritableStream::WriterState::Writable) { + writer->ReadyPromise()->MaybeResolveWithUndefined(); + } + + // Step 9. + // Perform ! WritableStreamDefaultControllerClose(stream.[[controller]]). + RefPtr<WritableStreamDefaultController> controller = aStream->Controller(); + WritableStreamDefaultControllerClose(aCx, controller, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 10. Return promise. + return promise.forget(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#ws-close +already_AddRefed<Promise> WritableStream::Close(JSContext* aCx, + ErrorResult& aRv) { + // Step 1. If ! IsWritableStreamLocked(this) is true, return a promise + // rejected with a TypeError exception. + if (Locked()) { + return Promise::CreateRejectedWithTypeError( + GetParentObject(), "Can not close locked stream"_ns, aRv); + } + + // Step 2. If ! WritableStreamCloseQueuedOrInFlight(this) is true, return a + // promise rejected with a TypeError exception. + if (CloseQueuedOrInFlight()) { + return Promise::CreateRejectedWithTypeError( + GetParentObject(), "Stream is already closing"_ns, aRv); + } + + // Step 3. Return ! WritableStreamClose(this). + RefPtr<WritableStream> thisRefPtr = this; + return WritableStreamClose(aCx, thisRefPtr, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#acquire-writable-stream-default-writer +already_AddRefed<WritableStreamDefaultWriter> +AcquireWritableStreamDefaultWriter(WritableStream* aStream, ErrorResult& aRv) { + // Step 1. Let writer be a new WritableStreamDefaultWriter. + RefPtr<WritableStreamDefaultWriter> writer = + new WritableStreamDefaultWriter(aStream->GetParentObject()); + + // Step 2. Perform ? SetUpWritableStreamDefaultWriter(writer, stream). + SetUpWritableStreamDefaultWriter(writer, aStream, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 3. Return writer. + return writer.forget(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#create-writable-stream +already_AddRefed<WritableStream> WritableStream::CreateAbstract( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSinkAlgorithmsBase* aAlgorithms, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv) { + // Step 1: Assert: ! IsNonNegativeNumber(highWaterMark) is true. + MOZ_ASSERT(IsNonNegativeNumber(aHighWaterMark)); + + // Step 2: Let stream be a new WritableStream. + // Step 3: Perform ! InitializeWritableStream(stream). + RefPtr<WritableStream> stream = new WritableStream( + aGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit); + + // Step 4: Let controller be a new WritableStreamDefaultController. + auto controller = + MakeRefPtr<WritableStreamDefaultController>(aGlobal, *stream); + + // Step 5: Perform ? SetUpWritableStreamDefaultController(stream, controller, + // startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, + // highWaterMark, sizeAlgorithm). + SetUpWritableStreamDefaultController(aCx, stream, controller, aAlgorithms, + aHighWaterMark, aSizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 6: Return stream. + return stream.forget(); +} + +already_AddRefed<WritableStreamDefaultWriter> WritableStream::GetWriter( + ErrorResult& aRv) { + return AcquireWritableStreamDefaultWriter(this, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#writable-stream-add-write-request +already_AddRefed<Promise> WritableStreamAddWriteRequest( + WritableStream* aStream) { + // Step 1. Assert: ! IsWritableStreamLocked(stream) is true. + MOZ_ASSERT(IsWritableStreamLocked(aStream)); + + // Step 2. Assert: stream.[[state]] is "writable". + MOZ_ASSERT(aStream->State() == WritableStream::WriterState::Writable); + + // Step 3. Let promise be a new promise. + RefPtr<Promise> promise = + Promise::CreateInfallible(aStream->GetParentObject()); + + // Step 4. Append promise to stream.[[writeRequests]]. + aStream->AppendWriteRequest(promise); + + // Step 5. Return promise. + return promise.forget(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#writablestream-set-up +// _BOUNDARY because `aAlgorithms->StartCallback` (called by +// SetUpWritableStreamDefaultController below) should not be able to run script +// in this case. +MOZ_CAN_RUN_SCRIPT_BOUNDARY void WritableStream::SetUpNative( + JSContext* aCx, UnderlyingSinkAlgorithmsWrapper& aAlgorithms, + Maybe<double> aHighWaterMark, QueuingStrategySize* aSizeAlgorithm, + ErrorResult& aRv) { + // an optional number highWaterMark (default 1) + double highWaterMark = aHighWaterMark.valueOr(1); + // and if given, highWaterMark must be a non-negative, non-NaN number. + MOZ_ASSERT(IsNonNegativeNumber(highWaterMark)); + + // Step 1: Let startAlgorithm be an algorithm that returns undefined. + // Step 2: Let closeAlgorithmWrapper be an algorithm that runs these steps: + // Step 3: Let abortAlgorithmWrapper be an algorithm that runs these steps: + // (Covered by UnderlyingSinkAlgorithmsWrapper) + + // Step 4: If sizeAlgorithm was not given, then set it to an algorithm that + // returns 1. (Callers will treat nullptr as such, see + // WritableStream::Constructor for details) + + // Step 5: Perform ! InitializeWritableStream(stream). + // (Covered by the constructor) + + // Step 6: Let controller be a new WritableStreamDefaultController. + auto controller = + MakeRefPtr<WritableStreamDefaultController>(GetParentObject(), *this); + + // Step 7: Perform ! SetUpWritableStreamDefaultController(stream, controller, + // startAlgorithm, writeAlgorithm, closeAlgorithmWrapper, + // abortAlgorithmWrapper, highWaterMark, sizeAlgorithm). + SetUpWritableStreamDefaultController(aCx, this, controller, &aAlgorithms, + highWaterMark, aSizeAlgorithm, aRv); +} + +already_AddRefed<WritableStream> WritableStream::CreateNative( + JSContext* aCx, nsIGlobalObject& aGlobal, + UnderlyingSinkAlgorithmsWrapper& aAlgorithms, Maybe<double> aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv) { + RefPtr<WritableStream> stream = new WritableStream( + &aGlobal, WritableStream::HoldDropJSObjectsCaller::Implicit); + stream->SetUpNative(aCx, aAlgorithms, aHighWaterMark, aSizeAlgorithm, aRv); + if (aRv.Failed()) { + return nullptr; + } + return stream.forget(); +} + +// https://streams.spec.whatwg.org/#writablestream-error +// To error a WritableStream stream given a JavaScript value e, perform ! +// WritableStreamDefaultControllerErrorIfNeeded(stream.[[controller]], e). +void WritableStream::ErrorNative(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // MOZ_KnownLive here instead of MOZ_KNOWN_LIVE at the field, because + // mController is set outside of the constructor + WritableStreamDefaultControllerErrorIfNeeded(aCx, MOZ_KnownLive(mController), + aError, aRv); +} + +} // namespace mozilla::dom diff --git a/dom/streams/WritableStream.h b/dom/streams/WritableStream.h new file mode 100644 index 0000000000..8b380020fe --- /dev/null +++ b/dom/streams/WritableStream.h @@ -0,0 +1,269 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WritableStream_h +#define mozilla_dom_WritableStream_h + +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/WritableStreamDefaultController.h" +#include "mozilla/dom/WritableStreamDefaultWriter.h" + +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class Promise; +class WritableStreamDefaultController; +class WritableStreamDefaultWriter; +class UnderlyingSinkAlgorithmsBase; +class UniqueMessagePortId; +class MessagePort; + +class WritableStream : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WritableStream) + + friend class ReadableStream; + + protected: + virtual ~WritableStream(); + + virtual void LastRelease() {} + + // If one extends WritableStream with another cycle collectable class, + // calling HoldJSObjects and DropJSObjects should happen using 'this' of + // that extending class. And in that case Explicit should be passed to the + // constructor of WriteableStream so that it doesn't make those calls. + // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1801214. + enum class HoldDropJSObjectsCaller { Implicit, Explicit }; + + explicit WritableStream(const GlobalObject& aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller); + explicit WritableStream(nsIGlobalObject* aGlobal, + HoldDropJSObjectsCaller aHoldDropCaller); + + public: + // Slot Getter/Setters: + bool Backpressure() const { return mBackpressure; } + void SetBackpressure(bool aBackpressure) { mBackpressure = aBackpressure; } + + Promise* GetCloseRequest() { return mCloseRequest; } + void SetCloseRequest(Promise* aRequest) { mCloseRequest = aRequest; } + + MOZ_KNOWN_LIVE WritableStreamDefaultController* Controller() { + return mController; + } + void SetController(WritableStreamDefaultController& aController) { + MOZ_ASSERT(!mController); + mController = &aController; + } + + Promise* GetInFlightWriteRequest() const { return mInFlightWriteRequest; } + + Promise* GetPendingAbortRequestPromise() const { + return mPendingAbortRequestPromise; + } + + void SetPendingAbortRequest(Promise* aPromise, JS::Handle<JS::Value> aReason, + bool aWasAlreadyErroring) { + mPendingAbortRequestPromise = aPromise; + mPendingAbortRequestReason = aReason; + mPendingAbortRequestWasAlreadyErroring = aWasAlreadyErroring; + } + + WritableStreamDefaultWriter* GetWriter() const { return mWriter; } + void SetWriter(WritableStreamDefaultWriter* aWriter) { mWriter = aWriter; } + + enum class WriterState { Writable, Closed, Erroring, Errored }; + + WriterState State() const { return mState; } + void SetState(const WriterState& aState) { mState = aState; } + + JS::Value StoredError() const { return mStoredError; } + void SetStoredError(JS::Handle<JS::Value> aStoredError) { + mStoredError = aStoredError; + } + + void AppendWriteRequest(RefPtr<Promise>& aRequest) { + mWriteRequests.AppendElement(aRequest); + } + + // CreateWritableStream + MOZ_CAN_RUN_SCRIPT static already_AddRefed<WritableStream> CreateAbstract( + JSContext* aCx, nsIGlobalObject* aGlobal, + UnderlyingSinkAlgorithmsBase* aAlgorithms, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + + // WritableStreamCloseQueuedOrInFlight + bool CloseQueuedOrInFlight() const { + return mCloseRequest || mInFlightCloseRequest; + } + + // WritableStreamDealWithRejection + MOZ_CAN_RUN_SCRIPT void DealWithRejection(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv); + + // WritableStreamFinishErroring + MOZ_CAN_RUN_SCRIPT void FinishErroring(JSContext* aCx, ErrorResult& aRv); + + // WritableStreamFinishInFlightClose + void FinishInFlightClose(); + + // WritableStreamFinishInFlightCloseWithError + MOZ_CAN_RUN_SCRIPT void FinishInFlightCloseWithError( + JSContext* aCx, JS::Handle<JS::Value> aError, ErrorResult& aRv); + + // WritableStreamFinishInFlightWrite + void FinishInFlightWrite(); + + // WritableStreamFinishInFlightWriteWithError + MOZ_CAN_RUN_SCRIPT void FinishInFlightWriteWithError( + JSContext* aCX, JS::Handle<JS::Value> aError, ErrorResult& aR); + + // WritableStreamHasOperationMarkedInFlight + bool HasOperationMarkedInFlight() const { + return mInFlightWriteRequest || mInFlightCloseRequest; + } + + // WritableStreamMarkCloseRequestInFlight + void MarkCloseRequestInFlight(); + + // WritableStreamMarkFirstWriteRequestInFlight + void MarkFirstWriteRequestInFlight(); + + // WritableStreamRejectCloseAndClosedPromiseIfNeeded + void RejectCloseAndClosedPromiseIfNeeded(); + + // WritableStreamStartErroring + MOZ_CAN_RUN_SCRIPT void StartErroring(JSContext* aCx, + JS::Handle<JS::Value> aReason, + ErrorResult& aRv); + + // WritableStreamUpdateBackpressure + void UpdateBackpressure(bool aBackpressure); + + // [Transferable] + // https://html.spec.whatwg.org/multipage/structured-data.html#transfer-steps + MOZ_CAN_RUN_SCRIPT bool Transfer(JSContext* aCx, + UniqueMessagePortId& aPortId); + // https://html.spec.whatwg.org/multipage/structured-data.html#transfer-receiving-steps + MOZ_CAN_RUN_SCRIPT static already_AddRefed<WritableStream> + ReceiveTransferImpl(JSContext* aCx, nsIGlobalObject* aGlobal, + MessagePort& aPort); + MOZ_CAN_RUN_SCRIPT static bool ReceiveTransfer( + JSContext* aCx, nsIGlobalObject* aGlobal, MessagePort& aPort, + JS::MutableHandle<JSObject*> aReturnObject); + + // Public functions to implement other specs + // https://streams.spec.whatwg.org/#other-specs-ws + + // https://streams.spec.whatwg.org/#writablestream-set-up + protected: + // Sets up the WritableStream. Intended for subclasses. + void SetUpNative(JSContext* aCx, UnderlyingSinkAlgorithmsWrapper& aAlgorithms, + Maybe<double> aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + + public: + // Creates and sets up a WritableStream. Use SetUpNative for this purpose in + // subclasses. + static already_AddRefed<WritableStream> CreateNative( + JSContext* aCx, nsIGlobalObject& aGlobal, + UnderlyingSinkAlgorithmsWrapper& aAlgorithms, + Maybe<double> aHighWaterMark, QueuingStrategySize* aSizeAlgorithm, + ErrorResult& aRv); + + // The following definitions must only be used on WritableStream instances + // initialized via the above set up algorithm: + + // https://streams.spec.whatwg.org/#writablestream-error + MOZ_CAN_RUN_SCRIPT void ErrorNative(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv); + + // IDL layer functions + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // IDL methods + + // TODO: Use MOZ_CAN_RUN_SCRIPT when IDL constructors can use it (bug 1749042) + MOZ_CAN_RUN_SCRIPT_BOUNDARY static already_AddRefed<WritableStream> + Constructor(const GlobalObject& aGlobal, + const Optional<JS::Handle<JSObject*>>& aUnderlyingSink, + const QueuingStrategy& aStrategy, ErrorResult& aRv); + + bool Locked() const { return !!mWriter; } + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Abort( + JSContext* cx, JS::Handle<JS::Value> aReason, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Close(JSContext* aCx, + ErrorResult& aRv); + + already_AddRefed<WritableStreamDefaultWriter> GetWriter(ErrorResult& aRv); + + protected: + nsCOMPtr<nsIGlobalObject> mGlobal; + + // Internal Slots: + private: + bool mBackpressure = false; + RefPtr<Promise> mCloseRequest; + RefPtr<WritableStreamDefaultController> mController; + RefPtr<Promise> mInFlightWriteRequest; + RefPtr<Promise> mInFlightCloseRequest; + + // We inline all members of [[pendingAbortRequest]] in this class. + // The absence (i.e. undefined) of the [[pendingAbortRequest]] + // is indicated by mPendingAbortRequestPromise = nullptr. + RefPtr<Promise> mPendingAbortRequestPromise; + JS::Heap<JS::Value> mPendingAbortRequestReason; + bool mPendingAbortRequestWasAlreadyErroring = false; + + WriterState mState = WriterState::Writable; + JS::Heap<JS::Value> mStoredError; + RefPtr<WritableStreamDefaultWriter> mWriter; + nsTArray<RefPtr<Promise>> mWriteRequests; + + HoldDropJSObjectsCaller mHoldDropCaller; +}; + +namespace streams_abstract { + +inline bool IsWritableStreamLocked(WritableStream* aStream) { + return aStream->Locked(); +} + +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WritableStreamAbort( + JSContext* aCx, WritableStream* aStream, JS::Handle<JS::Value> aReason, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WritableStreamClose( + JSContext* aCx, WritableStream* aStream, ErrorResult& aRv); + +already_AddRefed<Promise> WritableStreamAddWriteRequest( + WritableStream* aStream); + +already_AddRefed<WritableStreamDefaultWriter> +AcquireWritableStreamDefaultWriter(WritableStream* aStream, ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_WritableStream_h diff --git a/dom/streams/WritableStreamDefaultController.cpp b/dom/streams/WritableStreamDefaultController.cpp new file mode 100644 index 0000000000..c1ee4bd8ea --- /dev/null +++ b/dom/streams/WritableStreamDefaultController.cpp @@ -0,0 +1,564 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "js/Exception.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/dom/WritableStream.h" +#include "mozilla/dom/WritableStreamDefaultController.h" +#include "mozilla/dom/WritableStreamDefaultControllerBinding.h" +#include "mozilla/dom/UnderlyingSinkBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDebug.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +// Note: Using the individual macros vs NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE +// because I need to specificy a manual implementation of +// NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN. +NS_IMPL_CYCLE_COLLECTION_CLASS(WritableStreamDefaultController) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WritableStreamDefaultController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal, mSignal, mStrategySizeAlgorithm, + mAlgorithms, mStream) + tmp->mQueue.clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WritableStreamDefaultController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal, mSignal, mStrategySizeAlgorithm, + mAlgorithms, mStream) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WritableStreamDefaultController) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + // Trace the associated queue. + for (const auto& queueEntry : tmp->mQueue) { + aCallbacks.Trace(&queueEntry->mValue, "mQueue.mValue", aClosure); + } +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WritableStreamDefaultController) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WritableStreamDefaultController) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WritableStreamDefaultController) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +WritableStreamDefaultController::WritableStreamDefaultController( + nsISupports* aGlobal, WritableStream& aStream) + : mGlobal(do_QueryInterface(aGlobal)), mStream(&aStream) { + mozilla::HoldJSObjects(this); +} + +WritableStreamDefaultController::~WritableStreamDefaultController() { + // MG:XXX: LinkedLists are required to be empty at destruction, but it seems + // it is possible to have a controller be destructed while still + // having entries in its queue. + // + // This needs to be verified as not indicating some other issue. + mQueue.clear(); + mozilla::DropJSObjects(this); +} + +JSObject* WritableStreamDefaultController::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return WritableStreamDefaultController_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://streams.spec.whatwg.org/#ws-default-controller-error +void WritableStreamDefaultController::Error(JSContext* aCx, + JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + // Step 1. Let state be this.[[stream]].[[state]]. + // Step 2. If state is not "writable", return. + if (mStream->State() != WritableStream::WriterState::Writable) { + return; + } + // Step 3. Perform ! WritableStreamDefaultControllerError(this, e). + RefPtr<WritableStreamDefaultController> thisRefPtr = this; + WritableStreamDefaultControllerError(aCx, thisRefPtr, aError, aRv); +} + +// https://streams.spec.whatwg.org/#ws-default-controller-private-abort +already_AddRefed<Promise> WritableStreamDefaultController::AbortSteps( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1. Let result be the result of performing this.[[abortAlgorithm]], + // passing reason. + RefPtr<UnderlyingSinkAlgorithmsBase> algorithms = mAlgorithms; + Optional<JS::Handle<JS::Value>> optionalReason(aCx, aReason); + RefPtr<Promise> abortPromise = + algorithms->AbortCallback(aCx, optionalReason, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 2. Perform ! WritableStreamDefaultControllerClearAlgorithms(this). + ClearAlgorithms(); + + // Step 3. Return result. + return abortPromise.forget(); +} + +// https://streams.spec.whatwg.org/#ws-default-controller-private-error +void WritableStreamDefaultController::ErrorSteps() { + // Step 1. Perform ! ResetQueue(this). + ResetQueue(this); +} + +void WritableStreamDefaultController::SetSignal(AbortSignal* aSignal) { + MOZ_ASSERT(aSignal); + mSignal = aSignal; +} + +namespace streams_abstract { + +MOZ_CAN_RUN_SCRIPT static void +WritableStreamDefaultControllerAdvanceQueueIfNeeded( + JSContext* aCx, WritableStreamDefaultController* aController, + ErrorResult& aRv); + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller +void SetUpWritableStreamDefaultController( + JSContext* aCx, WritableStream* aStream, + WritableStreamDefaultController* aController, + UnderlyingSinkAlgorithmsBase* aAlgorithms, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv) { + // Step 1. Assert: stream implements WritableStream. + // Step 2. Assert: stream.[[controller]] is undefined. + MOZ_ASSERT(!aStream->Controller()); + + // Step 3. Set controller.[[stream]] to stream. + // Note: Already set in + // SetUpWritableStreamDefaultControllerFromUnderlyingSink. + MOZ_ASSERT(aController->Stream() == aStream); + + // Step 4. Set stream.[[controller]] to controller. + aStream->SetController(*aController); + + // Step 5. Perform ! ResetQueue(controller). + ResetQueue(aController); + + // Step 6. Set controller.[[signal]] to a new AbortSignal. + RefPtr<AbortSignal> signal = new AbortSignal(aController->GetParentObject(), + false, JS::UndefinedHandleValue); + aController->SetSignal(signal); + + // Step 7. Set controller.[[started]] to false. + aController->SetStarted(false); + + // Step 8. Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm. + aController->SetStrategySizeAlgorithm(aSizeAlgorithm); + + // Step 9. Set controller.[[strategyHWM]] to highWaterMark. + aController->SetStrategyHWM(aHighWaterMark); + + // Step 10. Set controller.[[writeAlgorithm]] to writeAlgorithm. + // Step 11. Set controller.[[closeAlgorithm]] to closeAlgorithm. + // Step 12. Set controller.[[abortAlgorithm]] to abortAlgorithm. + aController->SetAlgorithms(*aAlgorithms); + + // Step 13. Let backpressure be ! + // WritableStreamDefaultControllerGetBackpressure(controller). + bool backpressure = aController->GetBackpressure(); + + // Step 14. Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + aStream->UpdateBackpressure(backpressure); + + // Step 15. Let startResult be the result of performing startAlgorithm. (This + // may throw an exception.) + JS::Rooted<JS::Value> startResult(aCx, JS::UndefinedValue()); + RefPtr<WritableStreamDefaultController> controller(aController); + aAlgorithms->StartCallback(aCx, *controller, &startResult, aRv); + if (aRv.Failed()) { + return; + } + + // Step 16. Let startPromise be a promise resolved with startResult. + RefPtr<Promise> startPromise = + Promise::CreateInfallible(aStream->GetParentObject()); + startPromise->MaybeResolve(startResult); + + // Step 17/18. + startPromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + WritableStreamDefaultController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + // Step 17. Upon fulfillment of startPromise, + // Step 17.1. Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT(aController->Stream()->State() == + WritableStream::WriterState::Writable || + aController->Stream()->State() == + WritableStream::WriterState::Erroring); + // Step 17.2. Set controller.[[started]] to true. + aController->SetStarted(true); + // Step 17.3 Perform + // !WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + WritableStreamDefaultControllerAdvanceQueueIfNeeded( + aCx, MOZ_KnownLive(aController), aRv); + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + WritableStreamDefaultController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr<WritableStream> stream = aController->Stream(); + // Step 18. Upon rejection of startPromise with reason r, + // Step 18.1. Assert: stream.[[state]] is "writable" or "erroring". + MOZ_ASSERT( + stream->State() == WritableStream::WriterState::Writable || + stream->State() == WritableStream::WriterState::Erroring); + // Step 18.2. Set controller.[[started]] to true. + aController->SetStarted(true); + // Step 18.3. Perform ! WritableStreamDealWithRejection(stream, r). + stream->DealWithRejection(aCx, aValue, aRv); + }, + RefPtr(aController)); +} + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink +void SetUpWritableStreamDefaultControllerFromUnderlyingSink( + JSContext* aCx, WritableStream* aStream, + JS::Handle<JSObject*> aUnderlyingSink, UnderlyingSink& aUnderlyingSinkDict, + double aHighWaterMark, QueuingStrategySize* aSizeAlgorithm, + ErrorResult& aRv) { + // Step 1. + RefPtr<WritableStreamDefaultController> controller = + new WritableStreamDefaultController(aStream->GetParentObject(), *aStream); + + // Step 2 - 9. + auto algorithms = MakeRefPtr<UnderlyingSinkAlgorithms>( + aStream->GetParentObject(), aUnderlyingSink, aUnderlyingSinkDict); + + // Step 10. + SetUpWritableStreamDefaultController(aCx, aStream, controller, algorithms, + aHighWaterMark, aSizeAlgorithm, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-process-close +MOZ_CAN_RUN_SCRIPT static void WritableStreamDefaultControllerProcessClose( + JSContext* aCx, WritableStreamDefaultController* aController, + ErrorResult& aRv) { + // Step 1. Let stream be controller.[[stream]]. + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 2. Perform ! WritableStreamMarkCloseRequestInFlight(stream). + stream->MarkCloseRequestInFlight(); + + // Step 3. Perform ! DequeueValue(controller). + JS::Rooted<JS::Value> value(aCx); + DequeueValue(aController, &value); + + // Step 4. Assert: controller.[[queue]] is empty. + MOZ_ASSERT(aController->Queue().isEmpty()); + + // Step 5. Let sinkClosePromise be the result of performing + // controller.[[closeAlgorithm]]. + RefPtr<UnderlyingSinkAlgorithmsBase> algorithms = + aController->GetAlgorithms(); + RefPtr<Promise> sinkClosePromise = algorithms->CloseCallback(aCx, aRv); + if (aRv.Failed()) { + return; + } + + // Step 6. Perform ! + // WritableStreamDefaultControllerClearAlgorithms(controller). + aController->ClearAlgorithms(); + + // Step 7 + 8. + sinkClosePromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + WritableStreamDefaultController* aController) { + RefPtr<WritableStream> stream = aController->Stream(); + // Step 7. Upon fulfillment of sinkClosePromise, + // Step 7.1. Perform ! WritableStreamFinishInFlightClose(stream). + stream->FinishInFlightClose(); + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + WritableStreamDefaultController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr<WritableStream> stream = aController->Stream(); + // Step 8. Upon rejection of sinkClosePromise with reason reason, + // Step 8.1. Perform + // ! WritableStreamFinishInFlightCloseWithError(stream, reason). + stream->FinishInFlightCloseWithError(aCx, aValue, aRv); + }, + RefPtr(aController)); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-process-write +MOZ_CAN_RUN_SCRIPT static void WritableStreamDefaultControllerProcessWrite( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1. Let stream be controller.[[stream]]. + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 2. Perform ! WritableStreamMarkFirstWriteRequestInFlight(stream). + stream->MarkFirstWriteRequestInFlight(); + + // Step 3. Let sinkWritePromise be the result of performing + // controller.[[writeAlgorithm]], passing in chunk. + RefPtr<UnderlyingSinkAlgorithmsBase> algorithms = + aController->GetAlgorithms(); + RefPtr<Promise> sinkWritePromise = + algorithms->WriteCallback(aCx, aChunk, *aController, aRv); + if (aRv.Failed()) { + return; + } + + // Step 4 + 5: + sinkWritePromise->AddCallbacksWithCycleCollectedArgs( + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + WritableStreamDefaultController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 4.1. Perform ! WritableStreamFinishInFlightWrite(stream). + stream->FinishInFlightWrite(); + + // Step 4.2. Let state be stream.[[state]]. + WritableStream::WriterState state = stream->State(); + + // Step 4.3. Assert: state is "writable" or "erroring". + MOZ_ASSERT(state == WritableStream::WriterState::Writable || + state == WritableStream::WriterState::Erroring); + + // Step 4.4. Perform ! DequeueValue(controller). + JS::Rooted<JS::Value> value(aCx); + DequeueValue(aController, &value); + + // Step 4.5. If ! WritableStreamCloseQueuedOrInFlight(stream) is + // false and state is "writable", + if (!stream->CloseQueuedOrInFlight() && + state == WritableStream::WriterState::Writable) { + // Step 4.5.1. Let backpressure be ! + // WritableStreamDefaultControllerGetBackpressure(controller). + bool backpressure = aController->GetBackpressure(); + // Step 4.5.2. Perform ! WritableStreamUpdateBackpressure(stream, + // backpressure). + stream->UpdateBackpressure(backpressure); + } + + // Step 4.6. Perform ! + // WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + WritableStreamDefaultControllerAdvanceQueueIfNeeded( + aCx, MOZ_KnownLive(aController), aRv); + }, + [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, + WritableStreamDefaultController* aController) + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 5.1. If stream.[[state]] is "writable", perform ! + // WritableStreamDefaultControllerClearAlgorithms(controller). + if (stream->State() == WritableStream::WriterState::Writable) { + aController->ClearAlgorithms(); + } + + // Step 5.2. Perform ! + // WritableStreamFinishInFlightWriteWithError(stream, reason) + stream->FinishInFlightWriteWithError(aCx, aValue, aRv); + }, + RefPtr(aController)); +} + +// We use a JS::MagicValue to represent the close sentinel required by the spec. +// Normal JavaScript code can not generate magic values, so we can use this +// as a special value. However care has to be taken to not leak the magic value +// to other code. +constexpr JSWhyMagic CLOSE_SENTINEL = JS_GENERIC_MAGIC; + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-advance-queue-if-needed +static void WritableStreamDefaultControllerAdvanceQueueIfNeeded( + JSContext* aCx, WritableStreamDefaultController* aController, + ErrorResult& aRv) { + // Step 1. Let stream be controller.[[stream]]. + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 2. If controller.[[started]] is false, return. + if (!aController->Started()) { + return; + } + + // Step 3. If stream.[[inFlightWriteRequest]] is not undefined, return. + if (stream->GetInFlightWriteRequest()) { + return; + } + + // Step 4. Let state be stream.[[state]]. + WritableStream::WriterState state = stream->State(); + + // Step 5. Assert: state is not "closed" or "errored". + MOZ_ASSERT(state != WritableStream::WriterState::Closed && + state != WritableStream::WriterState::Errored); + + // Step 6. If state is "erroring", + if (state == WritableStream::WriterState::Erroring) { + // Step 6.1. Perform ! WritableStreamFinishErroring(stream). + stream->FinishErroring(aCx, aRv); + + // Step 6.2. Return. + return; + } + + // Step 7. If controller.[[queue]] is empty, return. + if (aController->Queue().isEmpty()) { + return; + } + + // Step 8. Let value be ! PeekQueueValue(controller). + JS::Rooted<JS::Value> value(aCx); + PeekQueueValue(aController, &value); + + // Step 9. If value is the close sentinel, perform ! + // WritableStreamDefaultControllerProcessClose(controller). + if (value.isMagic(CLOSE_SENTINEL)) { + WritableStreamDefaultControllerProcessClose(aCx, aController, aRv); + return; + } + + // Step 10. Otherwise, perform ! + // WritableStreamDefaultControllerProcessWrite(controller, value). + WritableStreamDefaultControllerProcessWrite(aCx, aController, value, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-close +void WritableStreamDefaultControllerClose( + JSContext* aCx, WritableStreamDefaultController* aController, + ErrorResult& aRv) { + // Step 1. Perform ! EnqueueValueWithSize(controller, close sentinel, 0). + JS::Rooted<JS::Value> aCloseSentinel(aCx, JS::MagicValue(CLOSE_SENTINEL)); + EnqueueValueWithSize(aController, aCloseSentinel, 0, aRv); + MOZ_ASSERT(!aRv.Failed()); + + // Step 2. Perform ! + // WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + WritableStreamDefaultControllerAdvanceQueueIfNeeded(aCx, aController, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-write +void WritableStreamDefaultControllerWrite( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, double chunkSize, ErrorResult& aRv) { + // Step 1. Let enqueueResult be EnqueueValueWithSize(controller, chunk, + // chunkSize). + IgnoredErrorResult rv; + EnqueueValueWithSize(aController, aChunk, chunkSize, rv); + + // Step 2. If enqueueResult is an abrupt completion, + if (rv.MaybeSetPendingException(aCx, + "WritableStreamDefaultController.write")) { + JS::Rooted<JS::Value> error(aCx); + JS_GetPendingException(aCx, &error); + JS_ClearPendingException(aCx); + + // Step 2.1. Perform ! + // WritableStreamDefaultControllerErrorIfNeeded(controller, + // enqueueResult.[[Value]]). + WritableStreamDefaultControllerErrorIfNeeded(aCx, aController, error, aRv); + + // Step 2.2. Return. + return; + } + + // Step 3. Let stream be controller.[[stream]]. + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 4. If ! WritableStreamCloseQueuedOrInFlight(stream) is false and + // stream.[[state]] is "writable", + if (!stream->CloseQueuedOrInFlight() && + stream->State() == WritableStream::WriterState::Writable) { + // Step 4.1. Let backpressure be + // !WritableStreamDefaultControllerGetBackpressure(controller). + bool backpressure = aController->GetBackpressure(); + + // Step 4.2. Perform ! WritableStreamUpdateBackpressure(stream, + // backpressure). + stream->UpdateBackpressure(backpressure); + } + + // Step 5. Perform + // ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + WritableStreamDefaultControllerAdvanceQueueIfNeeded(aCx, aController, aRv); +} + +void WritableStreamDefaultControllerError( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aError, ErrorResult& aRv) { + // Step 1. Let stream be controller.[[stream]]. + RefPtr<WritableStream> stream = aController->Stream(); + + // Step 2. Assert: stream.[[state]] is "writable". + MOZ_ASSERT(stream->State() == WritableStream::WriterState::Writable); + + // Step 3. Perform + // ! WritableStreamDefaultControllerClearAlgorithms(controller). + aController->ClearAlgorithms(); + + // Step 4.Perform ! WritableStreamStartErroring(stream, error). + stream->StartErroring(aCx, aError, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-error-if-needed +void WritableStreamDefaultControllerErrorIfNeeded( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aError, ErrorResult& aRv) { + // Step 1. If controller.[[stream]].[[state]] is "writable", perform + // !WritableStreamDefaultControllerError(controller, error). + if (aController->Stream()->State() == WritableStream::WriterState::Writable) { + WritableStreamDefaultControllerError(aCx, aController, aError, aRv); + } +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-get-chunk-size +double WritableStreamDefaultControllerGetChunkSize( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1. Let returnValue be the result of performing + // controller.[[strategySizeAlgorithm]], passing in chunk, and interpreting + // the result as a completion record. + RefPtr<QueuingStrategySize> sizeAlgorithm( + aController->StrategySizeAlgorithm()); + + // If !sizeAlgorithm, we return 1, which is inlined from + // https://streams.spec.whatwg.org/#make-size-algorithm-from-size-function + Optional<JS::Handle<JS::Value>> optionalChunk(aCx, aChunk); + + double chunkSize = + sizeAlgorithm + ? sizeAlgorithm->Call( + optionalChunk, aRv, + "WritableStreamDefaultController.[[strategySizeAlgorithm]]", + CallbackObject::eRethrowExceptions) + : 1.0; + + // Step 2. If returnValue is an abrupt completion, + if (aRv.MaybeSetPendingException( + aCx, "WritableStreamDefaultController.[[strategySizeAlgorithm]]")) { + JS::Rooted<JS::Value> error(aCx); + JS_GetPendingException(aCx, &error); + JS_ClearPendingException(aCx); + + // Step 2.1. Perform ! + // WritableStreamDefaultControllerErrorIfNeeded(controller, + // returnValue.[[Value]]). + WritableStreamDefaultControllerErrorIfNeeded(aCx, aController, error, aRv); + + // Step 2.2. Return 1. + return 1.0; + } + + // Step 3. Return returnValue.[[Value]]. + return chunkSize; +} + +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/WritableStreamDefaultController.h b/dom/streams/WritableStreamDefaultController.h new file mode 100644 index 0000000000..d41014594c --- /dev/null +++ b/dom/streams/WritableStreamDefaultController.h @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WritableStreamDefaultController_h +#define mozilla_dom_WritableStreamDefaultController_h + +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" +#include "mozilla/dom/QueueWithSizes.h" +#include "mozilla/dom/ReadRequest.h" +#include "mozilla/dom/UnderlyingSinkCallbackHelpers.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/Nullable.h" +#include "nsTArray.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +class AbortSignal; +class WritableStream; +struct UnderlyingSink; + +class WritableStreamDefaultController final : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WritableStreamDefaultController) + + explicit WritableStreamDefaultController(nsISupports* aGlobal, + WritableStream& aStream); + + protected: + ~WritableStreamDefaultController(); + + public: + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL methods/properties + + AbortSignal* Signal() { return mSignal; } + + MOZ_CAN_RUN_SCRIPT void Error(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv); + + // [[AbortSteps]] + MOZ_CAN_RUN_SCRIPT virtual already_AddRefed<Promise> AbortSteps( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv); + + // [[ErrorSteps]] + virtual void ErrorSteps(); + + // Internal Slot Accessors + + QueueWithSizes& Queue() { return mQueue; } + + double QueueTotalSize() const { return mQueueTotalSize; } + void SetQueueTotalSize(double aQueueTotalSize) { + mQueueTotalSize = aQueueTotalSize; + } + + void SetSignal(AbortSignal* aSignal); + + bool Started() const { return mStarted; } + void SetStarted(bool aStarted) { mStarted = aStarted; } + + double StrategyHWM() const { return mStrategyHWM; } + void SetStrategyHWM(double aStrategyHWM) { mStrategyHWM = aStrategyHWM; } + + QueuingStrategySize* StrategySizeAlgorithm() const { + return mStrategySizeAlgorithm; + } + void SetStrategySizeAlgorithm(QueuingStrategySize* aStrategySizeAlgorithm) { + mStrategySizeAlgorithm = aStrategySizeAlgorithm; + } + + UnderlyingSinkAlgorithmsBase* GetAlgorithms() { return mAlgorithms; } + void SetAlgorithms(UnderlyingSinkAlgorithmsBase& aAlgorithms) { + mAlgorithms = &aAlgorithms; + } + + WritableStream* Stream() { return mStream; } + + // WritableStreamDefaultControllerGetBackpressure + // https://streams.spec.whatwg.org/#writable-stream-default-controller-get-backpressure + bool GetBackpressure() const { + // Step 1. Let desiredSize be ! + // WritableStreamDefaultControllerGetDesiredSize(controller). + double desiredSize = GetDesiredSize(); + // Step 2. Return true if desiredSize ≤ 0, or false otherwise. + return desiredSize <= 0; + } + + // WritableStreamDefaultControllerGetDesiredSize + // https://streams.spec.whatwg.org/#writable-stream-default-controller-get-desired-size + double GetDesiredSize() const { return mStrategyHWM - mQueueTotalSize; } + + // WritableStreamDefaultControllerClearAlgorithms + // https://streams.spec.whatwg.org/#writable-stream-default-controller-clear-algorithms + void ClearAlgorithms() { + // Step 1. Set controller.[[writeAlgorithm]] to undefined. + // Step 2. Set controller.[[closeAlgorithm]] to undefined. + // Step 3. Set controller.[[abortAlgorithm]] to undefined. + // (As written in the spec, this can happen multiple time. Try running + // wpt/streams/transform-streams/errors.any.js for example.) + if (RefPtr<UnderlyingSinkAlgorithmsBase> algorithms = + mAlgorithms.forget()) { + algorithms->ReleaseObjects(); + } + + // Step 4. Set controller.[[strategySizeAlgorithm]] to undefined. + mStrategySizeAlgorithm = nullptr; + } + + private: + nsCOMPtr<nsIGlobalObject> mGlobal; + + // Internal Slots + QueueWithSizes mQueue = {}; + double mQueueTotalSize = 0.0; + RefPtr<AbortSignal> mSignal; + bool mStarted = false; + double mStrategyHWM = 0.0; + + RefPtr<QueuingStrategySize> mStrategySizeAlgorithm; + RefPtr<UnderlyingSinkAlgorithmsBase> mAlgorithms; + RefPtr<WritableStream> mStream; +}; + +namespace streams_abstract { + +MOZ_CAN_RUN_SCRIPT void SetUpWritableStreamDefaultController( + JSContext* aCx, WritableStream* aStream, + WritableStreamDefaultController* aController, + UnderlyingSinkAlgorithmsBase* aAlgorithms, double aHighWaterMark, + QueuingStrategySize* aSizeAlgorithm, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void SetUpWritableStreamDefaultControllerFromUnderlyingSink( + JSContext* aCx, WritableStream* aStream, + JS::Handle<JSObject*> aUnderlyingSink, UnderlyingSink& aUnderlyingSinkDict, + double aHighWaterMark, QueuingStrategySize* aSizeAlgorithm, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void WritableStreamDefaultControllerClose( + JSContext* aCx, WritableStreamDefaultController* aController, + ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void WritableStreamDefaultControllerWrite( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, double chunkSize, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void WritableStreamDefaultControllerError( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aError, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT void WritableStreamDefaultControllerErrorIfNeeded( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aError, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT double WritableStreamDefaultControllerGetChunkSize( + JSContext* aCx, WritableStreamDefaultController* aController, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_WritableStreamDefaultController_h diff --git a/dom/streams/WritableStreamDefaultWriter.cpp b/dom/streams/WritableStreamDefaultWriter.cpp new file mode 100644 index 0000000000..589eada7e2 --- /dev/null +++ b/dom/streams/WritableStreamDefaultWriter.cpp @@ -0,0 +1,544 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/WritableStreamDefaultWriter.h" +#include "js/Array.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/WritableStream.h" +#include "mozilla/dom/WritableStreamDefaultWriterBinding.h" +#include "nsCOMPtr.h" + +#include "mozilla/dom/Promise-inl.h" +#include "nsIGlobalObject.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +using namespace streams_abstract; + +NS_IMPL_CYCLE_COLLECTION_CLASS(WritableStreamDefaultWriter) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WritableStreamDefaultWriter) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal, mStream, mReadyPromise, + mClosedPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WritableStreamDefaultWriter) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal, mStream, mReadyPromise, + mClosedPromise) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WritableStreamDefaultWriter) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WritableStreamDefaultWriter) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WritableStreamDefaultWriter) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WritableStreamDefaultWriter) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +WritableStreamDefaultWriter::WritableStreamDefaultWriter( + nsIGlobalObject* aGlobal) + : mGlobal(aGlobal) { + mozilla::HoldJSObjects(this); +} + +WritableStreamDefaultWriter::~WritableStreamDefaultWriter() { + mozilla::DropJSObjects(this); +} + +void WritableStreamDefaultWriter::SetReadyPromise(Promise* aPromise) { + MOZ_ASSERT(aPromise); + mReadyPromise = aPromise; +} + +void WritableStreamDefaultWriter::SetClosedPromise(Promise* aPromise) { + MOZ_ASSERT(aPromise); + mClosedPromise = aPromise; +} + +JSObject* WritableStreamDefaultWriter::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return WritableStreamDefaultWriter_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +already_AddRefed<WritableStreamDefaultWriter> +WritableStreamDefaultWriter::Constructor(const GlobalObject& aGlobal, + WritableStream& aStream, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<WritableStreamDefaultWriter> writer = + new WritableStreamDefaultWriter(global); + SetUpWritableStreamDefaultWriter(writer, &aStream, aRv); + if (aRv.Failed()) { + return nullptr; + } + return writer.forget(); +} + +already_AddRefed<Promise> WritableStreamDefaultWriter::Closed() { + RefPtr<Promise> closedPromise = mClosedPromise; + return closedPromise.forget(); +} + +already_AddRefed<Promise> WritableStreamDefaultWriter::Ready() { + RefPtr<Promise> readyPromise = mReadyPromise; + return readyPromise.forget(); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#writable-stream-default-writer-get-desired-size +Nullable<double> WritableStreamDefaultWriterGetDesiredSize( + WritableStreamDefaultWriter* aWriter) { + // Step 1. Let stream be writer.[[stream]]. + RefPtr<WritableStream> stream = aWriter->GetStream(); + + // Step 2. Let state be stream.[[state]]. + WritableStream::WriterState state = stream->State(); + + // Step 3. If state is "errored" or "erroring", return null. + if (state == WritableStream::WriterState::Errored || + state == WritableStream::WriterState::Erroring) { + return nullptr; + } + + // Step 4. If state is "closed", return 0. + if (state == WritableStream::WriterState::Closed) { + return 0.0; + } + + // Step 5. Return + // ! WritableStreamDefaultControllerGetDesiredSize(stream.[[controller]]). + return stream->Controller()->GetDesiredSize(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#default-writer-desired-size +Nullable<double> WritableStreamDefaultWriter::GetDesiredSize(ErrorResult& aRv) { + // Step 1. If this.[[stream]] is undefined, throw a TypeError exception. + if (!mStream) { + aRv.ThrowTypeError("Missing stream"); + return nullptr; + } + + // Step 2. Return ! WritableStreamDefaultWriterGetDesiredSize(this). + RefPtr<WritableStreamDefaultWriter> thisRefPtr = this; + return WritableStreamDefaultWriterGetDesiredSize(thisRefPtr); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-writer-abort +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WritableStreamDefaultWriterAbort( + JSContext* aCx, WritableStreamDefaultWriter* aWriter, + JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1. Let stream be writer.[[stream]]. + RefPtr<WritableStream> stream = aWriter->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Return ! WritableStreamAbort(stream, reason). + return WritableStreamAbort(aCx, stream, aReason, aRv); +} + +// https://streams.spec.whatwg.org/#default-writer-abort +already_AddRefed<Promise> WritableStreamDefaultWriter::Abort( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv) { + // Step 1. If this.[[stream]] is undefined, return a promise rejected with a + // TypeError exception. + if (!mStream) { + aRv.ThrowTypeError("Missing stream"); + return nullptr; + } + + // Step 2. Return ! WritableStreamDefaultWriterAbort(this, reason). + RefPtr<WritableStreamDefaultWriter> thisRefPtr = this; + return WritableStreamDefaultWriterAbort(aCx, thisRefPtr, aReason, aRv); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-writer-close +MOZ_CAN_RUN_SCRIPT static already_AddRefed<Promise> +WritableStreamDefaultWriterClose(JSContext* aCx, + WritableStreamDefaultWriter* aWriter, + ErrorResult& aRv) { + // Step 1. Let stream be writer.[[stream]]. + RefPtr<WritableStream> stream = aWriter->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Return ! WritableStreamClose(stream). + return WritableStreamClose(aCx, stream, aRv); +} + +// https://streams.spec.whatwg.org/#default-writer-close +already_AddRefed<Promise> WritableStreamDefaultWriter::Close(JSContext* aCx, + ErrorResult& aRv) { + // Step 1. Let stream be this.[[stream]]. + RefPtr<WritableStream> stream = mStream; + + // Step 2. If stream is undefined, return a promise rejected with a TypeError + // exception. + if (!stream) { + aRv.ThrowTypeError("Missing stream"); + return nullptr; + } + + // Step 3. If ! WritableStreamCloseQueuedOrInFlight(stream) is true, + // return a promise rejected with a TypeError exception. + if (stream->CloseQueuedOrInFlight()) { + aRv.ThrowTypeError("Stream is closing"); + return nullptr; + } + + // Step 3. Return ! WritableStreamDefaultWriterClose(this). + RefPtr<WritableStreamDefaultWriter> thisRefPtr = this; + return WritableStreamDefaultWriterClose(aCx, thisRefPtr, aRv); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#writable-stream-default-writer-release +void WritableStreamDefaultWriterRelease(JSContext* aCx, + WritableStreamDefaultWriter* aWriter) { + // Step 1. Let stream be writer.[[stream]]. + RefPtr<WritableStream> stream = aWriter->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Assert: stream.[[writer]] is writer. + MOZ_ASSERT(stream->GetWriter() == aWriter); + + // Step 4. Let releasedError be a new TypeError. + JS::Rooted<JS::Value> releasedError(RootingCx(), JS::UndefinedValue()); + { + ErrorResult rv; + rv.ThrowTypeError("Releasing lock"); + bool ok = ToJSValue(aCx, std::move(rv), &releasedError); + MOZ_RELEASE_ASSERT(ok, "must be ok"); + } + + // Step 5. Perform ! + // WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, + // releasedError). + WritableStreamDefaultWriterEnsureReadyPromiseRejected(aWriter, releasedError); + + // Step 6. Perform ! + // WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, + // releasedError). + WritableStreamDefaultWriterEnsureClosedPromiseRejected(aWriter, + releasedError); + + // Step 7. Set stream.[[writer]] to undefined. + stream->SetWriter(nullptr); + + // Step 8. Set writer.[[stream]] to undefined. + aWriter->SetStream(nullptr); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#default-writer-release-lock +void WritableStreamDefaultWriter::ReleaseLock(JSContext* aCx) { + // Step 1. Let stream be this.[[stream]]. + RefPtr<WritableStream> stream = mStream; + + // Step 2. If stream is undefined, return. + if (!stream) { + return; + } + + // Step 3. Assert: stream.[[writer]] is not undefined. + MOZ_ASSERT(stream->GetWriter()); + + // Step 4. Perform ! WritableStreamDefaultWriterRelease(this). + RefPtr<WritableStreamDefaultWriter> thisRefPtr = this; + return WritableStreamDefaultWriterRelease(aCx, thisRefPtr); +} + +namespace streams_abstract { +// https://streams.spec.whatwg.org/#writable-stream-default-writer-write +already_AddRefed<Promise> WritableStreamDefaultWriterWrite( + JSContext* aCx, WritableStreamDefaultWriter* aWriter, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1. Let stream be writer.[[stream]]. + RefPtr<WritableStream> stream = aWriter->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Let controller be stream.[[controller]]. + RefPtr<WritableStreamDefaultController> controller = stream->Controller(); + + // Step 4. Let chunkSize be ! + // WritableStreamDefaultControllerGetChunkSize(controller, chunk). + double chunkSize = + WritableStreamDefaultControllerGetChunkSize(aCx, controller, aChunk, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 5. If stream is not equal to writer.[[stream]], return a promise + // rejected with a TypeError exception. + if (stream != aWriter->GetStream()) { + aRv.ThrowTypeError( + "Can not write on WritableStream owned by another writer."); + return nullptr; + } + + // Step 6. Let state be stream.[[state]]. + WritableStream::WriterState state = stream->State(); + + // Step 7. If state is "errored", return a promise rejected with + // stream.[[storedError]]. + if (state == WritableStream::WriterState::Errored) { + JS::Rooted<JS::Value> error(aCx, stream->StoredError()); + return Promise::CreateRejected(aWriter->GetParentObject(), error, aRv); + } + + // 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 (stream->CloseQueuedOrInFlight() || + state == WritableStream::WriterState::Closed) { + return Promise::CreateRejectedWithTypeError( + aWriter->GetParentObject(), "Stream is closed or closing"_ns, aRv); + } + + // Step 9. If state is "erroring", return a promise rejected with + // stream.[[storedError]]. + if (state == WritableStream::WriterState::Erroring) { + JS::Rooted<JS::Value> error(aCx, stream->StoredError()); + return Promise::CreateRejected(aWriter->GetParentObject(), error, aRv); + } + + // Step 10. Assert: state is "writable". + MOZ_ASSERT(state == WritableStream::WriterState::Writable); + + // Step 11. Let promise be ! WritableStreamAddWriteRequest(stream). + RefPtr<Promise> promise = WritableStreamAddWriteRequest(stream); + + // Step 12. Perform ! WritableStreamDefaultControllerWrite(controller, chunk, + // chunkSize). + WritableStreamDefaultControllerWrite(aCx, controller, aChunk, chunkSize, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 13. Return promise. + return promise.forget(); +} +} // namespace streams_abstract + +// https://streams.spec.whatwg.org/#default-writer-write +already_AddRefed<Promise> WritableStreamDefaultWriter::Write( + JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aRv) { + // Step 1. If this.[[stream]] is undefined, return a promise rejected with a + // TypeError exception. + if (!mStream) { + aRv.ThrowTypeError("Missing stream"); + return nullptr; + } + + // Step 2. Return ! WritableStreamDefaultWriterWrite(this, chunk). + return WritableStreamDefaultWriterWrite(aCx, this, aChunk, aRv); +} + +namespace streams_abstract { + +// https://streams.spec.whatwg.org/#set-up-writable-stream-default-writer +void SetUpWritableStreamDefaultWriter(WritableStreamDefaultWriter* aWriter, + WritableStream* aStream, + ErrorResult& aRv) { + // Step 1. If ! IsWritableStreamLocked(stream) is true, throw a TypeError + // exception. + if (IsWritableStreamLocked(aStream)) { + aRv.ThrowTypeError("WritableStream is already locked!"); + return; + } + + // Step 2. Set writer.[[stream]] to stream. + aWriter->SetStream(aStream); + + // Step 3. Set stream.[[writer]] to writer. + aStream->SetWriter(aWriter); + + // Step 4. Let state be stream.[[state]]. + WritableStream::WriterState state = aStream->State(); + + // Step 5. If state is "writable", + if (state == WritableStream::WriterState::Writable) { + RefPtr<Promise> readyPromise = + Promise::CreateInfallible(aWriter->GetParentObject()); + + // Step 5.1 If ! WritableStreamCloseQueuedOrInFlight(stream) is false and + // stream.[[backpressure]] is true, set writer.[[readyPromise]] to a new + // promise. + if (!aStream->CloseQueuedOrInFlight() && aStream->Backpressure()) { + aWriter->SetReadyPromise(readyPromise); + } else { + // Step 5.2. Otherwise, set writer.[[readyPromise]] to a promise resolved + // with undefined. + readyPromise->MaybeResolveWithUndefined(); + aWriter->SetReadyPromise(readyPromise); + } + + // Step 5.3. Set writer.[[closedPromise]] to a new promise. + RefPtr<Promise> closedPromise = + Promise::CreateInfallible(aWriter->GetParentObject()); + aWriter->SetClosedPromise(closedPromise); + } else if (state == WritableStream::WriterState::Erroring) { + // Step 6. Otherwise, if state is "erroring", + + // Step 6.1. Set writer.[[readyPromise]] to a promise rejected with + // stream.[[storedError]]. + JS::Rooted<JS::Value> storedError(RootingCx(), aStream->StoredError()); + RefPtr<Promise> readyPromise = + Promise::CreateInfallible(aWriter->GetParentObject()); + readyPromise->MaybeReject(storedError); + aWriter->SetReadyPromise(readyPromise); + + // Step 6.2. Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. + readyPromise->SetSettledPromiseIsHandled(); + + // Step 6.3. Set writer.[[closedPromise]] to a new promise. + RefPtr<Promise> closedPromise = + Promise::CreateInfallible(aWriter->GetParentObject()); + aWriter->SetClosedPromise(closedPromise); + } else if (state == WritableStream::WriterState::Closed) { + // Step 7. Otherwise, if state is "closed", + // Step 7.1. Set writer.[[readyPromise]] to a promise resolved with + // undefined. + RefPtr<Promise> readyPromise = + Promise::CreateResolvedWithUndefined(aWriter->GetParentObject(), aRv); + if (aRv.Failed()) { + return; + } + aWriter->SetReadyPromise(readyPromise); + + // Step 7.2. Set writer.[[closedPromise]] to a promise resolved with + // undefined. + RefPtr<Promise> closedPromise = + Promise::CreateResolvedWithUndefined(aWriter->GetParentObject(), aRv); + if (aRv.Failed()) { + return; + } + aWriter->SetClosedPromise(closedPromise); + } else { + // Step 8. Otherwise, + // Step 8.1 Assert: state is "errored". + MOZ_ASSERT(state == WritableStream::WriterState::Errored); + + // Step 8.2. Step Let storedError be stream.[[storedError]]. + JS::Rooted<JS::Value> storedError(RootingCx(), aStream->StoredError()); + + // Step 8.3. Set writer.[[readyPromise]] to a promise rejected with + // storedError. + RefPtr<Promise> readyPromise = + Promise::CreateInfallible(aWriter->GetParentObject()); + readyPromise->MaybeReject(storedError); + aWriter->SetReadyPromise(readyPromise); + + // Step 8.4. Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. + readyPromise->SetSettledPromiseIsHandled(); + + // Step 8.5. Set writer.[[closedPromise]] to a promise rejected with + // storedError. + RefPtr<Promise> closedPromise = + Promise::CreateInfallible(aWriter->GetParentObject()); + closedPromise->MaybeReject(storedError); + aWriter->SetClosedPromise(closedPromise); + + // Step 8.6 Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + closedPromise->SetSettledPromiseIsHandled(); + } +} + +// https://streams.spec.whatwg.org/#writable-stream-default-writer-ensure-closed-promise-rejected +void WritableStreamDefaultWriterEnsureClosedPromiseRejected( + WritableStreamDefaultWriter* aWriter, JS::Handle<JS::Value> aError) { + RefPtr<Promise> closedPromise = aWriter->ClosedPromise(); + // Step 1. If writer.[[closedPromise]].[[PromiseState]] is "pending", reject + // writer.[[closedPromise]] with error. + if (closedPromise->State() == Promise::PromiseState::Pending) { + closedPromise->MaybeReject(aError); + } else { + // Step 2. Otherwise, set writer.[[closedPromise]] to a promise rejected + // with error. + closedPromise = Promise::CreateInfallible(aWriter->GetParentObject()); + closedPromise->MaybeReject(aError); + aWriter->SetClosedPromise(closedPromise); + } + + // Step 3. Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + closedPromise->SetSettledPromiseIsHandled(); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-writer-ensure-ready-promise-rejected +void WritableStreamDefaultWriterEnsureReadyPromiseRejected( + WritableStreamDefaultWriter* aWriter, JS::Handle<JS::Value> aError) { + RefPtr<Promise> readyPromise = aWriter->ReadyPromise(); + // Step 1. If writer.[[readyPromise]].[[PromiseState]] is "pending", reject + // writer.[[readyPromise]] with error. + if (readyPromise->State() == Promise::PromiseState::Pending) { + readyPromise->MaybeReject(aError); + } else { + // Step 2. Otherwise, set writer.[[readyPromise]] to a promise rejected with + // error. + readyPromise = Promise::CreateInfallible(aWriter->GetParentObject()); + readyPromise->MaybeReject(aError); + aWriter->SetReadyPromise(readyPromise); + } + + // Step 3. Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. + readyPromise->SetSettledPromiseIsHandled(); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-writer-close-with-error-propagation +already_AddRefed<Promise> WritableStreamDefaultWriterCloseWithErrorPropagation( + JSContext* aCx, WritableStreamDefaultWriter* aWriter, ErrorResult& aRv) { + // Step 1. Let stream be writer.[[stream]]. + RefPtr<WritableStream> stream = aWriter->GetStream(); + + // Step 2. Assert: stream is not undefined. + MOZ_ASSERT(stream); + + // Step 3. Let state be stream.[[state]]. + WritableStream::WriterState state = stream->State(); + + // Step 4. If ! WritableStreamCloseQueuedOrInFlight(stream) is true + // or state is "closed", return a promise resolved with undefined. + if (stream->CloseQueuedOrInFlight() || + state == WritableStream::WriterState::Closed) { + return Promise::CreateResolvedWithUndefined(aWriter->GetParentObject(), + aRv); + } + + // Step 5. If state is "errored", + // return a promise rejected with stream.[[storedError]]. + if (state == WritableStream::WriterState::Errored) { + JS::Rooted<JS::Value> error(aCx, stream->StoredError()); + return Promise::CreateRejected(aWriter->GetParentObject(), error, aRv); + } + + // Step 6. Assert: state is "writable" or "erroring". + MOZ_ASSERT(state == WritableStream::WriterState::Writable || + state == WritableStream::WriterState::Erroring); + + // Step 7. Return ! WritableStreamDefaultWriterClose(writer). + return WritableStreamDefaultWriterClose(aCx, aWriter, aRv); +} + +} // namespace streams_abstract + +} // namespace mozilla::dom diff --git a/dom/streams/WritableStreamDefaultWriter.h b/dom/streams/WritableStreamDefaultWriter.h new file mode 100644 index 0000000000..487a504cd1 --- /dev/null +++ b/dom/streams/WritableStreamDefaultWriter.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_WritableStreamDefaultWriter_h +#define mozilla_dom_WritableStreamDefaultWriter_h + +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/QueuingStrategyBinding.h" + +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class Promise; +class WritableStream; + +class WritableStreamDefaultWriter final : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WritableStreamDefaultWriter) + + protected: + ~WritableStreamDefaultWriter(); + + public: + explicit WritableStreamDefaultWriter(nsIGlobalObject* aGlobal); + + // Slot Getter/Setters: + public: + WritableStream* GetStream() const { return mStream; } + void SetStream(WritableStream* aStream) { mStream = aStream; } + + Promise* ReadyPromise() const { return mReadyPromise; } + void SetReadyPromise(Promise* aPromise); + + Promise* ClosedPromise() const { return mClosedPromise; } + void SetClosedPromise(Promise* aPromise); + + public: + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // IDL Methods + static already_AddRefed<WritableStreamDefaultWriter> Constructor( + const GlobalObject& aGlobal, WritableStream& aStream, ErrorResult& aRv); + + already_AddRefed<Promise> Closed(); + already_AddRefed<Promise> Ready(); + + Nullable<double> GetDesiredSize(ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Abort( + JSContext* aCx, JS::Handle<JS::Value> aReason, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Close(JSContext* aCx, + ErrorResult& aRv); + + void ReleaseLock(JSContext* aCx); + + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> Write( + JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aRv); + + // Internal Slots: + private: + nsCOMPtr<nsIGlobalObject> mGlobal; + RefPtr<WritableStream> mStream; + RefPtr<Promise> mReadyPromise; + RefPtr<Promise> mClosedPromise; +}; + +namespace streams_abstract { + +void SetUpWritableStreamDefaultWriter(WritableStreamDefaultWriter* aWriter, + WritableStream* aStream, + ErrorResult& aRv); + +void WritableStreamDefaultWriterEnsureClosedPromiseRejected( + WritableStreamDefaultWriter* aWriter, JS::Handle<JS::Value> aError); + +void WritableStreamDefaultWriterEnsureReadyPromiseRejected( + WritableStreamDefaultWriter* aWriter, JS::Handle<JS::Value> aError); + +Nullable<double> WritableStreamDefaultWriterGetDesiredSize( + WritableStreamDefaultWriter* aWriter); + +void WritableStreamDefaultWriterRelease(JSContext* aCx, + WritableStreamDefaultWriter* aWriter); + +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WritableStreamDefaultWriterWrite( + JSContext* aCx, WritableStreamDefaultWriter* aWriter, + JS::Handle<JS::Value> aChunk, ErrorResult& aRv); + +MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> +WritableStreamDefaultWriterCloseWithErrorPropagation( + JSContext* aCx, WritableStreamDefaultWriter* aWriter, ErrorResult& aRv); + +} // namespace streams_abstract + +} // namespace mozilla::dom + +#endif // mozilla_dom_WritableStreamDefaultWriter_h diff --git a/dom/streams/crashtests/1764222.html b/dom/streams/crashtests/1764222.html new file mode 100644 index 0000000000..a48a1168a1 --- /dev/null +++ b/dom/streams/crashtests/1764222.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> + // This can't be in WPT because of bug 1766711. + let a = new AbortController() + a.abort() + let b = new File(['ó ¬a'], 'a', {}) + let c = new WritableStream() + b.stream().tee()[0].pipeTo(c, { 'signal': a.signal }) +</script> diff --git a/dom/streams/crashtests/crashtests.list b/dom/streams/crashtests/crashtests.list new file mode 100644 index 0000000000..c19e7107dc --- /dev/null +++ b/dom/streams/crashtests/crashtests.list @@ -0,0 +1 @@ +load 1764222.html diff --git a/dom/streams/moz.build b/dom/streams/moz.build new file mode 100644 index 0000000000..454a32b789 --- /dev/null +++ b/dom/streams/moz.build @@ -0,0 +1,72 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Streams") + +EXPORTS.mozilla.dom += [ + "BaseQueuingStrategy.h", + "ByteLengthQueuingStrategy.h", + "ByteStreamHelpers.h", + "CountQueuingStrategy.h", + "QueueWithSizes.h", + "ReadableByteStreamController.h", + "ReadableStream.h", + "ReadableStreamBYOBReader.h", + "ReadableStreamBYOBRequest.h", + "ReadableStreamController.h", + "ReadableStreamDefaultController.h", + "ReadableStreamDefaultReader.h", + "ReadableStreamGenericReader.h", + "ReadRequest.h", + "TransformerCallbackHelpers.h", + "TransformStream.h", + "TransformStreamDefaultController.h", + "UnderlyingSinkCallbackHelpers.h", + "UnderlyingSourceCallbackHelpers.h", + "WritableStream.h", + "WritableStreamDefaultController.h", + "WritableStreamDefaultWriter.h", +] + +UNIFIED_SOURCES += [ + "ByteLengthQueuingStrategy.cpp", + "ByteStreamHelpers.cpp", + "CountQueuingStrategy.cpp", + "ReadableByteStreamController.cpp", + "ReadableStream.cpp", + "ReadableStreamBYOBReader.cpp", + "ReadableStreamBYOBRequest.cpp", + "ReadableStreamDefaultController.cpp", + "ReadableStreamDefaultReader.cpp", + "ReadableStreamPipeTo.cpp", + "ReadableStreamTee.cpp", + "StreamUtils.cpp", + "TeeState.cpp", + "Transferable.cpp", + "TransformerCallbackHelpers.cpp", + "TransformStream.cpp", + "TransformStreamDefaultController.cpp", + "UnderlyingSinkCallbackHelpers.cpp", + "UnderlyingSourceCallbackHelpers.cpp", + "WritableStream.cpp", + "WritableStreamDefaultController.cpp", + "WritableStreamDefaultWriter.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") # to import MessagePort.h + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/ipc", +] + +# MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] +# ROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] diff --git a/dom/streams/test/xpcshell/bug-1387503-1.js b/dom/streams/test/xpcshell/bug-1387503-1.js new file mode 100644 index 0000000000..777f614c44 --- /dev/null +++ b/dom/streams/test/xpcshell/bug-1387503-1.js @@ -0,0 +1,44 @@ +// Test uncatchable error when a stream source's pull() method is called. +let readerCreated = false; +let fnFinished = false; +let g; + +add_task(async function test() { + // Make `debugger;` raise an uncatchable error. + g = newGlobal({ newCompartment: true }); + g.parent = this; + g.hit = false; + g.eval( + ` new Debugger(parent).onDebuggerStatement = _frame => (hit = true, null);` + ); + + // Create a stream whose pull() method raises an uncatchable error, + // and try reading from it. + + async function fn() { + try { + let stream = new ReadableStream({ + start(controller) {}, + pull(controller) { + // eslint-disable-next-line no-debugger + debugger; + }, + }); + + let reader = stream.getReader(); + let p = reader.read(); + readerCreated = true; + await p; + } finally { + fnFinished = true; + } + } + + fn(); +}); + +add_task(() => { + equal(readerCreated, true); + equal(g.hit, true); + equal(fnFinished, false); +}); diff --git a/dom/streams/test/xpcshell/bug-1387503-2.js b/dom/streams/test/xpcshell/bug-1387503-2.js new file mode 100644 index 0000000000..1678b46649 --- /dev/null +++ b/dom/streams/test/xpcshell/bug-1387503-2.js @@ -0,0 +1,52 @@ +// Test uncatchable error when a stream's queuing strategy's size() method is called. +/* global newGlobal */ + +let fnFinished = false; +let g; +add_task(async function test() { + // Make `debugger;` raise an uncatchable exception. + g = newGlobal(); + g.parent = this; + g.hit = false; + g.info = info; + g.eval(` + var dbg = new Debugger(parent); + dbg.onDebuggerStatement = (_frame, exc) => {hit = true; info("hit"); return null}; +`); + + async function fn() { + // Await once to postpone the uncatchable error until we're running inside + // a reaction job. We don't want the rest of the test to be terminated. + // (`drainJobQueue` catches uncatchable errors!) + await 1; + + try { + // Create a stream with a strategy whose .size() method raises an + // uncatchable exception, and have it call that method. + new ReadableStream( + { + start(controller) { + controller.enqueue("FIRST POST"); // this calls .size() + }, + }, + { + size() { + // eslint-disable-next-line no-debugger + debugger; + }, + } + ); + } finally { + fnFinished = true; + } + } + + fn() + .then(() => info("Resolved")) + .catch(() => info("Rejected")); +}); + +add_task(() => { + equal(g.hit, true, "We hit G"); + equal(fnFinished, false, "We didn't hit the finally block"); +}); diff --git a/dom/streams/test/xpcshell/bug-1503406.js b/dom/streams/test/xpcshell/bug-1503406.js new file mode 100644 index 0000000000..8e45291faf --- /dev/null +++ b/dom/streams/test/xpcshell/bug-1503406.js @@ -0,0 +1,20 @@ +let read; +let reader; + +add_task(async function test() { + let g = newGlobal({ wantGlobalProperties: ["ReadableStream"] }); + reader = g.eval(` + let stream = new ReadableStream({ + start(controller) { + controller.enqueue([]); + }, + }); + let [b1, b2] = stream.tee(); + b1.getReader(); +`); + read = new ReadableStream({}).getReader().read; +}); + +add_task(async function test2() { + read.call(reader); +}); diff --git a/dom/streams/test/xpcshell/bug-1773237.js b/dom/streams/test/xpcshell/bug-1773237.js new file mode 100644 index 0000000000..0d0107fe85 --- /dev/null +++ b/dom/streams/test/xpcshell/bug-1773237.js @@ -0,0 +1,11 @@ +// This test fails if there is an unhandled promise rejection +var stream = new ReadableStream({ + pull() { + return Promise.reject("foobar"); + }, +}); +var response = new Response(stream); +var text = response.text().then( + () => {}, + e => {} +); diff --git a/dom/streams/test/xpcshell/dom_stream_prototype_test.js b/dom/streams/test/xpcshell/dom_stream_prototype_test.js new file mode 100644 index 0000000000..b127368318 --- /dev/null +++ b/dom/streams/test/xpcshell/dom_stream_prototype_test.js @@ -0,0 +1,29 @@ +"use strict"; + +var log = []; +const stream = new ReadableStream({ + start(controller) { + log.push("started"); + }, + pull(controller) { + log.push("pulled"); + controller.enqueue("hi from pull"); + }, + cancel() { + log.push("cancelled"); + }, +}); + +print(log); // Currently prints "started"! + +add_task(async function helper() { + var reader = stream.getReader(); + var readPromise = reader.read(); + readPromise.then(x => print(`Printing promise result ${x}, log ${log}`)); + print(log); + + var x = await readPromise; + print(`Promise result ${x} ${x.value}`); + Assert.equal(x.value, "hi from pull"); + Assert.ok(true); +}); diff --git a/dom/streams/test/xpcshell/fetch.js b/dom/streams/test/xpcshell/fetch.js new file mode 100644 index 0000000000..af414735f0 --- /dev/null +++ b/dom/streams/test/xpcshell/fetch.js @@ -0,0 +1,35 @@ +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.init(this); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task(async function helper() { + do_get_profile(); + + // The SearchService is also needed in order to construct the initial state, + // which means that the AddonManager needs to be available. + await AddonTestUtils.promiseStartupManager(); + + // The example.com domain will be used to host the dynamic layout JSON and + // the top stories JSON. + let server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + server.registerDirectory("/", do_get_cwd()); + + Assert.equal(true, fetch instanceof Function); + var k = await fetch("http://example.com/"); + console.log(k); + console.log(k.body); + var r = k.body.getReader(); + console.log(r); + var v = await r.read(); + console.log(v); +}); diff --git a/dom/streams/test/xpcshell/head.js b/dom/streams/test/xpcshell/head.js new file mode 100644 index 0000000000..510cabe757 --- /dev/null +++ b/dom/streams/test/xpcshell/head.js @@ -0,0 +1,45 @@ +"use strict"; + +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); + +const SYSTEM_PRINCIPAL = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal +); + +function addTestingFunctionsToGlobal(global) { + global.eval( + ` + const testingFunctions = Cu.getJSTestingFunctions(); + for (let k in testingFunctions) { + + this[k] = testingFunctions[k]; + } + ` + ); + if (!global.print) { + global.print = info; + } + if (!global.newGlobal) { + global.newGlobal = newGlobal; + } + if (!global.Debugger) { + addDebuggerToGlobal(global); + } +} + +addTestingFunctionsToGlobal(this); + +/* Create a new global, with all the JS shell testing functions. Similar to the + * newGlobal function exposed to JS shells, and useful for porting JS shell + * tests to xpcshell tests. + */ +function newGlobal(args) { + const global = new Cu.Sandbox(SYSTEM_PRINCIPAL, { + freshCompartment: true, + ...args, + }); + addTestingFunctionsToGlobal(global); + return global; +} diff --git a/dom/streams/test/xpcshell/large-pipeto.js b/dom/streams/test/xpcshell/large-pipeto.js new file mode 100644 index 0000000000..420adfea20 --- /dev/null +++ b/dom/streams/test/xpcshell/large-pipeto.js @@ -0,0 +1,101 @@ +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// Stamp an array buffer with a pattern; verified by verify_chunk below. +function init(array) { + for (var i = 0; i < array.length; i++) { + array[i] = i % 256; + } +} + +// The construction of the file below ends up with 12 instances +// of the array buffer -- we want this to be larger than 2**32 bytes +// to exercise potential truncation of nsIInputStream::Read's count +// parameter. +const ABLENGTH = (2 ** 32 + 8) / 12; + +// Get a large file (bigger than 2GB!) +function get_file() { + const array = new ArrayBuffer(ABLENGTH); + const buff = new Uint8Array(array); + + // Stamp with pattern. + init(buff); + + const blob = new Blob([buff, buff], {}); + return new File([blob, blob, blob, blob, blob, blob], {}); +} + +// Verify that the chunks the stream recieve correspond to the initialization +function verify_chunk(chunk, verification_state) { + for (var j = 0; j < chunk.length; j++) { + // If we don't match the fill pattern. + if (chunk[j] != verification_state.expected) { + // we ran out of array buffer; so we should be looking at the first byte of the array again. + if ( + verification_state.total_index % ABLENGTH != 0 || + chunk[j] != 0 /* ASSUME THAT THE INITIALIZATION OF THE BUFFER IS ZERO */ + ) { + throw new Error( + `Mismatch: chunk[${j}] (${chunk[j]}) != ${verification_state.expected} (total_index ${verification_state.total_index})` + ); + } + // Reset the fill expectation to 1 for next round. + verification_state.expected = 1; + } else { + // We are inside regular fill section + verification_state.expected = (verification_state.expected + 1) % 256; + } + verification_state.total_index++; + } +} + +// Pipe To Testing: Win32 can't handle the file size created in this test and OOMs. +add_task( + { + skip_if: () => AppConstants.platform == "win" && !Services.appinfo.is64Bit, + }, + async () => { + var chunk_verification_state = { + expected: 0, + total_index: 0, + }; + + const file = get_file(); + + await file.stream().pipeTo( + new WritableStream({ + write(chunk) { + verify_chunk(chunk, chunk_verification_state); + }, + }) + ); + } +); + +// Do the same test as above, but this time don't use pipeTo. +add_task( + { + skip_if: () => AppConstants.platform == "win" && !Services.appinfo.is64Bit, + }, + async () => { + var file = get_file(); + + var chunk_verification_state = { + expected: 0, + total_index: 0, + }; + + var streamReader = file.stream().getReader(); + + while (true) { + var res = await streamReader.read(); + if (res.done) { + break; + } + var chunk = res.value; + verify_chunk(chunk, chunk_verification_state); + } + } +); diff --git a/dom/streams/test/xpcshell/proper-realm-cancel.js b/dom/streams/test/xpcshell/proper-realm-cancel.js new file mode 100644 index 0000000000..1de6db3172 --- /dev/null +++ b/dom/streams/test/xpcshell/proper-realm-cancel.js @@ -0,0 +1,5 @@ +// This test passes if we don't have a CCW assertion. +var g = newGlobal(); +var ccwCancelMethod = new g.Function("return 17;"); + +new ReadableStream({ cancel: ccwCancelMethod }).cancel("bye"); diff --git a/dom/streams/test/xpcshell/proper-realm-pull.js b/dom/streams/test/xpcshell/proper-realm-pull.js new file mode 100644 index 0000000000..9d42d9a65d --- /dev/null +++ b/dom/streams/test/xpcshell/proper-realm-pull.js @@ -0,0 +1,6 @@ +// This test passes if we don't have a CCW assertion. + +var g = newGlobal({ newCompartment: true }); +var ccwPullMethod = new g.Function("return 17;"); + +new ReadableStream({ pull: ccwPullMethod }); diff --git a/dom/streams/test/xpcshell/response.js b/dom/streams/test/xpcshell/response.js new file mode 100644 index 0000000000..ead1be527f --- /dev/null +++ b/dom/streams/test/xpcshell/response.js @@ -0,0 +1,30 @@ +"use strict"; + +add_task(async function (test) { + return new Response(new Blob([], { type: "text/plain" })).body.cancel(); +}); + +add_task(function (test) { + var response = new Response( + new Blob(["This is data"], { type: "text/plain" }) + ); + var reader = response.body.getReader(); + reader.read(); + return reader.cancel(); +}); + +add_task(function (test) { + var response = new Response(new Blob(["T"], { type: "text/plain" })); + var reader = response.body.getReader(); + + var closedPromise = reader.closed.then(function () { + return reader.cancel(); + }); + reader.read().then(function readMore({ done, value }) { + if (!done) { + return reader.read().then(readMore); + } + return undefined; + }); + return closedPromise; +}); diff --git a/dom/streams/test/xpcshell/subclassing.js b/dom/streams/test/xpcshell/subclassing.js new file mode 100644 index 0000000000..b2b86cf353 --- /dev/null +++ b/dom/streams/test/xpcshell/subclassing.js @@ -0,0 +1,123 @@ +// Adapted from js/src/tests/non262/ReadableStream/subclassing.js to suit requirements of xpcshell-testing. + +function assertEq(a, b) { + Assert.equal(a, b); +} +function assertThrowsInstanceOf(fun, err) { + var regexp = new RegExp(err.name); + print(regexp); + Assert.throws(fun, regexp); +} + +// Spot-check subclassing of stream constructors. + +// ReadableStream can be subclassed. +class PartyStreamer extends ReadableStream {} + +let started = false; +add_task(function subclass_helper() { + // The base class constructor is called. + let stream = new PartyStreamer({ + // (The ReadableStream constructor calls this start method.) + start(c) { + started = true; + }, + }); + + assertEq(started, true); + + // The instance's prototype chain is correct. + assertEq(stream.__proto__, PartyStreamer.prototype); + assertEq(stream.__proto__.__proto__, ReadableStream.prototype); + assertEq(stream.__proto__.__proto__.__proto__, Object.prototype); + assertEq(stream.__proto__.__proto__.__proto__.__proto__, null); + assertEq(stream instanceof ReadableStream, true); + + // Non-generic methods can be called on the resulting stream. + stream.getReader(); + assertEq(stream.locked, true); +}); + +add_task(function strategy_helper() { + // CountQueuingStrategy can be subclassed. + class PixelStrategy extends CountQueuingStrategy {} + assertEq( + new PixelStrategy({ highWaterMark: 4 }).__proto__, + PixelStrategy.prototype + ); + + // The base class constructor is called. + assertThrowsInstanceOf(() => new PixelStrategy(), TypeError); + assertEq(new PixelStrategy({ highWaterMark: -1 }).highWaterMark, -1); + + // // VerySmartStrategy can be subclassed. + // class VerySmartStrategy extends ByteLengthQueuingStrategy { + // size(chunk) { + // return super.size(chunk) * 8; + // } + // } + // let vss = new VerySmartStrategy({ highWaterMark: 12 }); + // assertEq(vss.size(new ArrayBuffer(8)), 64); + // assertEq(vss.__proto__, VerySmartStrategy.prototype); +}); + +// Even ReadableStreamDefaultReader can be subclassed. +add_task(async function readerTest() { + const ReadableStreamDefaultReader = new ReadableStream().getReader() + .constructor; + class MindReader extends ReadableStreamDefaultReader { + async read() { + let foretold = { value: "death", done: false }; + let actual = await super.read(); + actual = foretold; // ZOMG I WAS RIGHT, EXACTLY AS FORETOLD they should call me a righter + return actual; + } + } + + let stream = new ReadableStream({ + start(c) { + c.enqueue("one"); + c.enqueue("two"); + }, + pull(c) { + c.close(); + }, + }); + let reader = new MindReader(stream); + let result = await reader.read(); + assertEq(result.value, "death"); + reader.releaseLock(); + + reader = stream.getReader(); + result = await reader.read(); + assertEq(result.done, false); + assertEq(result.value, "two"); + result = await reader.read(); + assertEq(result.done, true); + assertEq(result.value, undefined); +}); + +add_task(function default_controller() { + // Even ReadableStreamDefaultController, which can't be constructed, + // can be subclassed. + let ReadableStreamDefaultController; + new ReadableStream({ + start(c) { + ReadableStreamDefaultController = c.constructor; + }, + }); + class MasterController extends ReadableStreamDefaultController { + constructor() { + // don't call super, it'll just throw + return Object.create(MasterController.prototype); + } + } + let c = new MasterController(); + + // The prototype chain is per spec. + assertEq(c instanceof ReadableStreamDefaultController, true); + + // But the instance does not have the internal slots of a + // ReadableStreamDefaultController, so the non-generic methods can't be used. + assertThrowsInstanceOf(() => c.enqueue("horse"), TypeError); +}); diff --git a/dom/streams/test/xpcshell/too-big-array-buffer.js b/dom/streams/test/xpcshell/too-big-array-buffer.js new file mode 100644 index 0000000000..b80c36e813 --- /dev/null +++ b/dom/streams/test/xpcshell/too-big-array-buffer.js @@ -0,0 +1,15 @@ +add_task(async function helper() { + // Note: this test assumes the largest possible ArrayBuffer is + // smaller than 10GB -- if that changes, this test will fail. + let rs = new ReadableStream({ + type: "bytes", + autoAllocateChunkSize: 10 * 1024 * 1024 * 1024, + }); + let reader = rs.getReader(); + try { + await reader.read(); + Assert.equal(true, false, "Shouldn't succeed at reading"); + } catch (e) { + Assert.equal(e instanceof RangeError, true, "Should throw RangeError"); + } +}); diff --git a/dom/streams/test/xpcshell/xpcshell.toml b/dom/streams/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..3fe0232d17 --- /dev/null +++ b/dom/streams/test/xpcshell/xpcshell.toml @@ -0,0 +1,32 @@ +[DEFAULT] +head = "head.js" +skip-if = ["os == 'android'"] +support-files = "" + +["bug-1387503-1.js"] +prefs = ["security.allow_parent_unrestricted_js_loads=true"] + +["bug-1503406.js"] + +["bug-1773237.js"] + +["dom_stream_prototype_test.js"] + +["fetch.js"] + +["large-pipeto.js"] +skip-if = [ + "os == 'win'", + "tsan", # Causes claim expired errors; see Bug 1770170. +] +run-sequentially = "very high failure rate in parallel" + +["proper-realm-cancel.js"] + +["proper-realm-pull.js"] + +["response.js"] + +["subclassing.js"] + +["too-big-array-buffer.js"] |