diff options
Diffstat (limited to 'dom/fs/api/FileSystemWritableFileStream.cpp')
-rw-r--r-- | dom/fs/api/FileSystemWritableFileStream.cpp | 1043 |
1 files changed, 1043 insertions, 0 deletions
diff --git a/dom/fs/api/FileSystemWritableFileStream.cpp b/dom/fs/api/FileSystemWritableFileStream.cpp new file mode 100644 index 0000000000..ab133b2707 --- /dev/null +++ b/dom/fs/api/FileSystemWritableFileStream.cpp @@ -0,0 +1,1043 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemWritableFileStream.h" + +#include "fs/FileSystemAsyncCopy.h" +#include "fs/FileSystemShutdownBlocker.h" +#include "fs/FileSystemThreadSafeStreamOwner.h" +#include "mozilla/Buffer.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/InputStreamLengthHelper.h" +#include "mozilla/MozPromise.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/dom/Blob.h" +#include "mozilla/dom/FileSystemHandle.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemManager.h" +#include "mozilla/dom/FileSystemWritableFileStreamBinding.h" +#include "mozilla/dom/FileSystemWritableFileStreamChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WritableStreamDefaultController.h" +#include "mozilla/dom/fs/TargetPtrHolder.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ipc/RandomAccessStreamUtils.h" +#include "nsAsyncStreamCopier.h" +#include "nsIInputStream.h" +#include "nsIRequestObserver.h" +#include "nsISupportsImpl.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +namespace { + +CopyableErrorResult RejectWithConvertedErrors(nsresult aRv) { + CopyableErrorResult err; + switch (aRv) { + case NS_ERROR_DOM_FILE_NOT_FOUND_ERR: + [[fallthrough]]; + case NS_ERROR_FILE_NOT_FOUND: + err.ThrowNotFoundError("File not found"); + break; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + err.ThrowQuotaExceededError("Quota exceeded"); + break; + default: + err.Throw(aRv); + } + + return err; +} + +RefPtr<FileSystemWritableFileStream::WriteDataPromise> ResolvePromise( + const Int64Promise::ResolveOrRejectValue& aValue) { + MOZ_ASSERT(aValue.IsResolve()); + return FileSystemWritableFileStream::WriteDataPromise::CreateAndResolve( + Some(aValue.ResolveValue()), __func__); +} + +RefPtr<FileSystemWritableFileStream::WriteDataPromise> ResolvePromise( + const BoolPromise::ResolveOrRejectValue& aValue) { + MOZ_ASSERT(aValue.IsResolve()); + return FileSystemWritableFileStream::WriteDataPromise::CreateAndResolve( + Nothing(), __func__); +} + +class WritableFileStreamUnderlyingSinkAlgorithms final + : public UnderlyingSinkAlgorithmsWrapper { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) + + explicit WritableFileStreamUnderlyingSinkAlgorithms( + FileSystemWritableFileStream& aStream) + : mStream(&aStream) {} + + already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) override; + + already_AddRefed<Promise> CloseCallbackImpl(JSContext* aCx, + ErrorResult& aRv) override; + + already_AddRefed<Promise> AbortCallbackImpl( + JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, + ErrorResult& aRv) override; + + void ReleaseObjects() override; + + private: + ~WritableFileStreamUnderlyingSinkAlgorithms() = default; + + RefPtr<FileSystemWritableFileStream> mStream; +}; + +} // namespace + +class FileSystemWritableFileStream::Command { + public: + explicit Command(RefPtr<FileSystemWritableFileStream> aWritableFileStream) + : mWritableFileStream(std::move(aWritableFileStream)) { + MOZ_ASSERT(mWritableFileStream); + } + + NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStream::Command) + + private: + ~Command() { mWritableFileStream->NoteFinishedCommand(); } + + RefPtr<FileSystemWritableFileStream> mWritableFileStream; +}; + +class FileSystemWritableFileStream::CloseHandler { + enum struct State : uint8_t { Initial = 0, Open, Closing, Closed }; + + public: + CloseHandler() + : mShutdownBlocker(fs::FileSystemShutdownBlocker::CreateForWritable()), + mClosePromiseHolder(), + mState(State::Initial) {} + + NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStream::CloseHandler) + + /** + * @brief Are we not yet closing? + */ + bool IsOpen() const { return State::Open == mState; } + + /** + * @brief Are we not open and not closed? + */ + bool IsClosing() const { return State::Closing == mState; } + + /** + * @brief Are we already fully closed? + */ + bool IsClosed() const { return State::Closed == mState; } + + /** + * @brief Transition from open to closing state + * + * @return true if the state was open and became closing after the call + * @return false in all the other cases the previous state is preserved + */ + bool SetClosing() { + const bool isOpen = State::Open == mState; + + if (isOpen) { + mState = State::Closing; + } + + return isOpen; + } + + RefPtr<BoolPromise> GetClosePromise() const { + MOZ_ASSERT(State::Open != mState, + "Please call SetClosing before GetClosePromise"); + + if (State::Closing == mState) { + return mClosePromiseHolder.Ensure(__func__); + } + + // Instant resolve for initial state due to early shutdown or closed state + return BoolPromise::CreateAndResolve(true, __func__); + } + + /** + * @brief Transition from initial to open state. In initial state + * + */ + void Open() { + MOZ_ASSERT(State::Initial == mState); + mShutdownBlocker->Block(); + + mState = State::Open; + } + + /** + * @brief Transition to closed state and resolve all pending promises. + * + */ + void Close() { + mShutdownBlocker->Unblock(); + mState = State::Closed; + mClosePromiseHolder.ResolveIfExists(true, __func__); + } + + protected: + virtual ~CloseHandler() = default; + + private: + RefPtr<fs::FileSystemShutdownBlocker> mShutdownBlocker; + + mutable MozPromiseHolder<BoolPromise> mClosePromiseHolder; + + State mState; +}; + +FileSystemWritableFileStream::FileSystemWritableFileStream( + const nsCOMPtr<nsIGlobalObject>& aGlobal, + RefPtr<FileSystemManager>& aManager, + mozilla::ipc::RandomAccessStreamParams&& aStreamParams, + RefPtr<FileSystemWritableFileStreamChild> aActor, + already_AddRefed<TaskQueue> aTaskQueue, + const fs::FileSystemEntryMetadata& aMetadata) + : WritableStream(aGlobal, HoldDropJSObjectsCaller::Explicit), + mManager(aManager), + mActor(std::move(aActor)), + mTaskQueue(aTaskQueue), + mStreamParams(std::move(aStreamParams)), + mMetadata(std::move(aMetadata)), + mCloseHandler(MakeAndAddRef<CloseHandler>()), + mCommandActive(false) { + LOG(("Created WritableFileStream %p", this)); + + // Connect with the actor directly in the constructor. This way the actor + // can call `FileSystemWritableFileStream::ClearActor` when we call + // `PFileSystemWritableFileStreamChild::Send__delete__` even when + // FileSystemWritableFileStream::Create fails, in which case the not yet + // fully constructed FileSystemWritableFileStream is being destroyed. + mActor->SetStream(this); + + mozilla::HoldJSObjects(this); +} + +FileSystemWritableFileStream::~FileSystemWritableFileStream() { + MOZ_ASSERT(!mCommandActive); + MOZ_ASSERT(IsDone()); + + mozilla::DropJSObjects(this); +} + +// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream +// * This is fallible because of OOM handling of JSAPI. See bug 1762233. +// XXX(krosylight): _BOUNDARY because SetUpNative here can't run script because +// StartCallback here is no-op. Can we let the static check automatically detect +// this situation? +/* static */ +MOZ_CAN_RUN_SCRIPT_BOUNDARY +Result<RefPtr<FileSystemWritableFileStream>, nsresult> +FileSystemWritableFileStream::Create( + const nsCOMPtr<nsIGlobalObject>& aGlobal, + RefPtr<FileSystemManager>& aManager, + mozilla::ipc::RandomAccessStreamParams&& aStreamParams, + RefPtr<FileSystemWritableFileStreamChild> aActor, + const fs::FileSystemEntryMetadata& aMetadata) { + MOZ_ASSERT(aGlobal); + + QM_TRY_UNWRAP(auto streamTransportService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>, + MOZ_SELECT_OVERLOAD(do_GetService), + NS_STREAMTRANSPORTSERVICE_CONTRACTID)); + + RefPtr<TaskQueue> taskQueue = + TaskQueue::Create(streamTransportService.forget(), "WritableStreamQueue"); + MOZ_ASSERT(taskQueue); + + AutoJSAPI jsapi; + if (!jsapi.Init(aGlobal)) { + return Err(NS_ERROR_FAILURE); + } + JSContext* cx = jsapi.cx(); + + // Step 1. Let stream be a new FileSystemWritableFileStream in realm. + // Step 2. Set stream.[[file]] to file. (Covered by the constructor) + RefPtr<FileSystemWritableFileStream> stream = + new FileSystemWritableFileStream( + aGlobal, aManager, std::move(aStreamParams), std::move(aActor), + taskQueue.forget(), aMetadata); + + auto autoClose = MakeScopeExit([stream] { + stream->mCloseHandler->Close(); + stream->mActor->SendClose(/* aAbort */ true); + }); + + QM_TRY_UNWRAP( + RefPtr<StrongWorkerRef> workerRef, + ([stream]() -> Result<RefPtr<StrongWorkerRef>, nsresult> { + WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate(); + if (!workerPrivate) { + return RefPtr<StrongWorkerRef>(); + } + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + workerPrivate, "FileSystemWritableFileStream::Create", [stream]() { + if (stream->IsOpen()) { + // We don't need the promise, we just + // begin the closing process. + Unused << stream->BeginAbort(); + } + }); + QM_TRY(MOZ_TO_RESULT(workerRef)); + + return workerRef; + }())); + + // Step 3 - 5 + auto algorithms = + MakeRefPtr<WritableFileStreamUnderlyingSinkAlgorithms>(*stream); + + // Step 8: Set up stream with writeAlgorithm set to writeAlgorithm, + // closeAlgorithm set to closeAlgorithm, abortAlgorithm set to + // abortAlgorithm, highWaterMark set to highWaterMark, and + // sizeAlgorithm set to sizeAlgorithm. + IgnoredErrorResult rv; + stream->SetUpNative(cx, *algorithms, + // Step 6. Let highWaterMark be 1. + Some(1), + // Step 7. Let sizeAlgorithm be an algorithm + // that returns 1. (nullptr returns 1, See + // WritableStream::Constructor for details) + nullptr, rv); + if (rv.Failed()) { + return Err(rv.StealNSResult()); + } + + autoClose.release(); + + stream->mWorkerRef = std::move(workerRef); + stream->mCloseHandler->Open(); + + // Step 9: Return stream. + return stream; +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(FileSystemWritableFileStream, + WritableStream) + +NS_IMPL_CYCLE_COLLECTION_CLASS(FileSystemWritableFileStream) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileSystemWritableFileStream, + WritableStream) + // Per the comment for the FileSystemManager class, don't unlink mManager! + if (tmp->IsOpen()) { + Unused << tmp->BeginAbort(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileSystemWritableFileStream, + WritableStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void FileSystemWritableFileStream::LastRelease() { + // We can't call `FileSystemWritableFileStream::Close` here because it may + // need to keep FileSystemWritableFileStream object alive which isn't possible + // when the object is about to be deleted. There are other mechanisms which + // ensure that the object is correctly closed before destruction. For example + // the object unlinking and the worker shutdown (we get notified about it via + // the callback passed to `StrongWorkerRef`) are used to close the object if + // it hasn't been closed yet. + + if (mActor) { + PFileSystemWritableFileStreamChild::Send__delete__(mActor); + MOZ_ASSERT(!mActor); + } +} + +RefPtr<FileSystemWritableFileStream::Command> +FileSystemWritableFileStream::CreateCommand() { + MOZ_ASSERT(!mCommandActive); + + mCommandActive = true; + + return MakeRefPtr<Command>(this); +} + +bool FileSystemWritableFileStream::IsCommandActive() const { + return mCommandActive; +} + +void FileSystemWritableFileStream::ClearActor() { + MOZ_ASSERT(mActor); + + mActor = nullptr; +} + +bool FileSystemWritableFileStream::IsOpen() const { + return mCloseHandler->IsOpen(); +} + +bool FileSystemWritableFileStream::IsFinishing() const { + return mCloseHandler->IsClosing(); +} + +bool FileSystemWritableFileStream::IsDone() const { + return mCloseHandler->IsClosed(); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::BeginFinishing( + bool aShouldAbort) { + using ClosePromise = PFileSystemWritableFileStreamChild::ClosePromise; + MOZ_ASSERT(IsOpen()); + + if (mCloseHandler->SetClosing()) { + Finish() + ->Then(mTaskQueue, __func__, + [selfHolder = fs::TargetPtrHolder(this)]() mutable { + if (selfHolder->mStreamOwner) { + selfHolder->mStreamOwner->Close(); + } else { + // If the stream was not deserialized, `mStreamParams` still + // contains a pre-opened file descriptor which needs to be + // closed here by moving `mStreamParams` to a local variable + // (the file descriptor will be closed for real when + // `streamParams` goes out of scope). + + mozilla::ipc::RandomAccessStreamParams streamParams( + std::move(selfHolder->mStreamParams)); + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) { + return self->mTaskQueue->BeginShutdown(); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [aShouldAbort, self = RefPtr(this)]( + const ShutdownPromise::ResolveOrRejectValue& /* aValue */) { + if (!self->mActor) { + return ClosePromise::CreateAndResolve(void_t(), __func__); + } + + return self->mActor->SendClose(aShouldAbort); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)]( + const ClosePromise::ResolveOrRejectValue& aValue) { + self->mWorkerRef = nullptr; + self->mCloseHandler->Close(); + + QM_TRY(OkIf(aValue.IsResolve()), QM_VOID); + }); + } + + return mCloseHandler->GetClosePromise(); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::BeginClose() { + MOZ_ASSERT(IsOpen()); + return BeginFinishing(/* aShouldAbort */ false); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::BeginAbort() { + MOZ_ASSERT(IsOpen()); + return BeginFinishing(/* aShouldAbort */ true); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::OnDone() { + MOZ_ASSERT(!IsOpen()); + + return mCloseHandler->GetClosePromise(); +} + +already_AddRefed<Promise> FileSystemWritableFileStream::Write( + JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aError) { + // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream + // Step 3. Let writeAlgorithm be an algorithm which takes a chunk argument + // and returns the result of running the write a chunk algorithm with stream + // and chunk. + + aError.MightThrowJSException(); + + // https://fs.spec.whatwg.org/#write-a-chunk + // Step 1. Let input be the result of converting chunk to a + // FileSystemWriteChunkType. + + ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams data; + if (!data.Init(aCx, aChunk)) { + aError.StealExceptionFromJSContext(aCx); + return nullptr; + } + + // Step 2. Let p be a new promise. + RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError); + if (aError.Failed()) { + return nullptr; + } + + RefPtr<Promise> innerPromise = Promise::Create(GetParentObject(), aError); + if (aError.Failed()) { + return nullptr; + } + + RefPtr<Command> command = CreateCommand(); + + // Step 3.3. + Write(data)->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr{this}, command, + promise](const WriteDataPromise::ResolveOrRejectValue& aValue) { + MOZ_ASSERT(!aValue.IsNothing()); + if (aValue.IsResolve()) { + const Maybe<int64_t>& maybeWritten = aValue.ResolveValue(); + if (maybeWritten.isSome()) { + promise->MaybeResolve(maybeWritten.value()); + return; + } + + promise->MaybeResolveWithUndefined(); + return; + } + + CopyableErrorResult err = aValue.RejectValue(); + + if (self->IsOpen()) { + self->BeginAbort()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, err = std::move(err)]( + const BoolPromise::ResolveOrRejectValue&) mutable { + // Do not capture command to this context: + // close cannot proceed + promise->MaybeReject(std::move(err)); + }); + } else if (self->IsFinishing()) { + self->OnDone()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, err = std::move(err)]( + const BoolPromise::ResolveOrRejectValue&) mutable { + // Do not capture command to this context: + // close cannot proceed + promise->MaybeReject(std::move(err)); + }); + + } else { + promise->MaybeReject(std::move(err)); + } + }); + + return promise.forget(); +} + +RefPtr<FileSystemWritableFileStream::WriteDataPromise> +FileSystemWritableFileStream::Write( + ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData) { + auto rejectWithTypeError = [](const auto& aMessage) { + CopyableErrorResult err; + err.ThrowTypeError(aMessage); + return WriteDataPromise::CreateAndReject(err, __func__); + }; + + auto rejectWithSyntaxError = [](const auto& aMessage) { + CopyableErrorResult err; + err.ThrowSyntaxError(aMessage); + return WriteDataPromise::CreateAndReject(err, __func__); + }; + + if (!IsOpen()) { + return rejectWithTypeError("WritableFileStream closed"); + } + + auto tryResolve = [self = RefPtr{this}](const auto& aValue) + -> RefPtr<FileSystemWritableFileStream::WriteDataPromise> { + MOZ_ASSERT(self->IsCommandActive()); + + if (aValue.IsResolve()) { + return ResolvePromise(aValue); + } + + MOZ_ASSERT(aValue.IsReject()); + return WriteDataPromise::CreateAndReject( + RejectWithConvertedErrors(aValue.RejectValue()), __func__); + }; + + auto tryResolveInt64 = + [tryResolve](const Int64Promise::ResolveOrRejectValue& aValue) { + return tryResolve(aValue); + }; + + auto tryResolveBool = + [tryResolve](const BoolPromise::ResolveOrRejectValue& aValue) { + return tryResolve(aValue); + }; + + // Step 3.3. Let command be input.type if input is a WriteParams, ... + if (aData.IsWriteParams()) { + const WriteParams& params = aData.GetAsWriteParams(); + switch (params.mType) { + // Step 3.4. If command is "write": + case WriteCommandType::Write: { + if (!params.mData.WasPassed()) { + return rejectWithSyntaxError("write() requires data"); + } + + // Step 3.4.2. If data is undefined, reject p with a TypeError and + // abort. + if (params.mData.Value().IsNull()) { + return rejectWithTypeError("write() of null data"); + } + + Maybe<uint64_t> position; + + if (params.mPosition.WasPassed()) { + if (params.mPosition.Value().IsNull()) { + return rejectWithTypeError("write() with null position"); + } + + position = Some(params.mPosition.Value().Value()); + } + + return Write(params.mData.Value().Value(), position) + ->Then(GetCurrentSerialEventTarget(), __func__, + std::move(tryResolveInt64)); + } + + // Step 3.5. Otherwise, if command is "seek": + case WriteCommandType::Seek: + if (!params.mPosition.WasPassed()) { + return rejectWithSyntaxError("seek() requires a position"); + } + + // Step 3.5.1. If chunk.position is undefined, reject p with a + // TypeError and abort. + if (params.mPosition.Value().IsNull()) { + return rejectWithTypeError("seek() with null position"); + } + + return Seek(params.mPosition.Value().Value()) + ->Then(GetCurrentSerialEventTarget(), __func__, + std::move(tryResolveBool)); + + // Step 3.6. Otherwise, if command is "truncate": + case WriteCommandType::Truncate: + if (!params.mSize.WasPassed()) { + return rejectWithSyntaxError("truncate() requires a size"); + } + + // Step 3.6.1. If chunk.size is undefined, reject p with a TypeError + // and abort. + if (params.mSize.Value().IsNull()) { + return rejectWithTypeError("truncate() with null size"); + } + + return Truncate(params.mSize.Value().Value()) + ->Then(GetCurrentSerialEventTarget(), __func__, + std::move(tryResolveBool)); + + default: + MOZ_CRASH("Bad WriteParams value!"); + } + } + + // Step 3.3. ... and "write" otherwise. + // Step 3.4. If command is "write": + return Write(aData, Nothing()) + ->Then(GetCurrentSerialEventTarget(), __func__, + std::move(tryResolveInt64)); +} + +// WebIDL Boilerplate + +JSObject* FileSystemWritableFileStream::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return FileSystemWritableFileStream_Binding::Wrap(aCx, this, aGivenProto); +} + +// WebIDL Interface + +already_AddRefed<Promise> FileSystemWritableFileStream::Write( + const ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData, + ErrorResult& aError) { + // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-write + // Step 1. Let writer be the result of getting a writer for this. + RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError); + if (aError.Failed()) { + return nullptr; + } + + // Step 2. Let result be the result of writing a chunk to writer given data. + AutoJSAPI jsapi; + if (!jsapi.Init(GetParentObject())) { + aError.ThrowUnknownError("Internal error"); + return nullptr; + } + + JSContext* cx = jsapi.cx(); + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + + JS::Rooted<JS::Value> val(cx); + if (!aData.ToJSVal(cx, global, &val)) { + aError.ThrowUnknownError("Internal error"); + return nullptr; + } + + RefPtr<Promise> promise = writer->Write(cx, val, aError); + + // Step 3. Release writer. + writer->ReleaseLock(cx); + + // Step 4. Return result. + return promise.forget(); +} + +already_AddRefed<Promise> FileSystemWritableFileStream::Seek( + uint64_t aPosition, ErrorResult& aError) { + // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-seek + // Step 1. Let writer be the result of getting a writer for this. + RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError); + if (aError.Failed()) { + return nullptr; + } + + // Step 2. Let result be the result of writing a chunk to writer given + // «[ "type" → "seek", "position" → position ]». + AutoJSAPI jsapi; + if (!jsapi.Init(GetParentObject())) { + aError.ThrowUnknownError("Internal error"); + return nullptr; + } + + JSContext* cx = jsapi.cx(); + + RootedDictionary<WriteParams> writeParams(cx); + writeParams.mType = WriteCommandType::Seek; + writeParams.mPosition.Construct(aPosition); + + JS::Rooted<JS::Value> val(cx); + if (!ToJSValue(cx, writeParams, &val)) { + aError.ThrowUnknownError("Internal error"); + return nullptr; + } + + RefPtr<Promise> promise = writer->Write(cx, val, aError); + + // Step 3. Release writer. + writer->ReleaseLock(cx); + + // Step 4. Return result. + return promise.forget(); +} + +already_AddRefed<Promise> FileSystemWritableFileStream::Truncate( + uint64_t aSize, ErrorResult& aError) { + // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-truncate + // Step 1. Let writer be the result of getting a writer for this. + RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError); + if (aError.Failed()) { + return nullptr; + } + + // Step 2. Let result be the result of writing a chunk to writer given + // «[ "type" → "truncate", "size" → size ]». + AutoJSAPI jsapi; + if (!jsapi.Init(GetParentObject())) { + aError.ThrowUnknownError("Internal error"); + return nullptr; + } + + JSContext* cx = jsapi.cx(); + + RootedDictionary<WriteParams> writeParams(cx); + writeParams.mType = WriteCommandType::Truncate; + writeParams.mSize.Construct(aSize); + + JS::Rooted<JS::Value> val(cx); + if (!ToJSValue(cx, writeParams, &val)) { + aError.ThrowUnknownError("Internal error"); + return nullptr; + } + + RefPtr<Promise> promise = writer->Write(cx, val, aError); + + // Step 3. Release writer. + writer->ReleaseLock(cx); + + // Step 4. Return result. + return promise.forget(); +} + +template <typename T> +RefPtr<Int64Promise> FileSystemWritableFileStream::Write( + const T& aData, const Maybe<uint64_t> aPosition) { + MOZ_ASSERT(IsOpen()); + + nsCOMPtr<nsIInputStream> inputStream; + + // https://fs.spec.whatwg.org/#write-a-chunk + // Step 3.4.6 If data is a BufferSource, let dataBytes be a copy of data. + auto vectorFromTypedArray = CreateFromTypedArrayData<Vector<uint8_t>>(aData); + if (vectorFromTypedArray.isSome()) { + Maybe<Vector<uint8_t>>& maybeVector = vectorFromTypedArray.ref(); + QM_TRY(MOZ_TO_RESULT(maybeVector.isSome()), CreateAndRejectInt64Promise); + + // Here we copy + + size_t length = maybeVector->length(); + QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream( + getter_AddRefs(inputStream), + AsChars(Span(maybeVector->extractOrCopyRawBuffer(), length)), + NS_ASSIGNMENT_ADOPT)), + CreateAndRejectInt64Promise); + + return WriteImpl(std::move(inputStream), aPosition); + } + + // Step 3.4.7 Otherwise, if data is a Blob ... + if (aData.IsBlob()) { + Blob& blob = aData.GetAsBlob(); + + ErrorResult error; + blob.CreateInputStream(getter_AddRefs(inputStream), error); + QM_TRY((MOZ_TO_RESULT(!error.Failed()).mapErr([&error](const nsresult rv) { + return error.StealNSResult(); + })), + CreateAndRejectInt64Promise); + + return WriteImpl(std::move(inputStream), aPosition); + } + + // Step 3.4.8 Otherwise ... + MOZ_ASSERT(aData.IsUTF8String()); + + // Here we copy + nsCString dataString; + if (!dataString.Assign(aData.GetAsUTF8String(), mozilla::fallible)) { + return Int64Promise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__); + } + + // Input stream takes ownership + QM_TRY(MOZ_TO_RESULT(NS_NewCStringInputStream(getter_AddRefs(inputStream), + std::move(dataString))), + CreateAndRejectInt64Promise); + + return WriteImpl(std::move(inputStream), aPosition); +} + +RefPtr<Int64Promise> FileSystemWritableFileStream::WriteImpl( + nsCOMPtr<nsIInputStream> aInputStream, const Maybe<uint64_t> aPosition) { + return InvokeAsync( + mTaskQueue, __func__, + [selfHolder = fs::TargetPtrHolder(this), + inputStream = std::move(aInputStream), aPosition]() { + QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), + CreateAndRejectInt64Promise); + + if (aPosition.isSome()) { + LOG(("%p: Seeking to %" PRIu64, selfHolder->mStreamOwner.get(), + aPosition.value())); + + QM_TRY( + MOZ_TO_RESULT(selfHolder->mStreamOwner->Seek(aPosition.value())), + CreateAndRejectInt64Promise); + } + + nsCOMPtr<nsIOutputStream> streamSink = + selfHolder->mStreamOwner->OutputStream(); + + auto written = std::make_shared<int64_t>(0); + auto writingProgress = [written](uint32_t aDelta) { + *written += static_cast<int64_t>(aDelta); + }; + + auto promiseHolder = MakeUnique<MozPromiseHolder<Int64Promise>>(); + RefPtr<Int64Promise> promise = promiseHolder->Ensure(__func__); + + auto writingCompletion = + [written, + promiseHolder = std::move(promiseHolder)](nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + promiseHolder->ResolveIfExists(*written, __func__); + return; + } + + promiseHolder->RejectIfExists(aStatus, __func__); + }; + + QM_TRY(MOZ_TO_RESULT(fs::AsyncCopy( + inputStream, streamSink, selfHolder->mTaskQueue, + nsAsyncCopyMode::NS_ASYNCCOPY_VIA_READSEGMENTS, + /* aCloseSource */ true, /* aCloseSink */ false, + std::move(writingProgress), std::move(writingCompletion))), + CreateAndRejectInt64Promise); + + return promise; + }); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::Seek(uint64_t aPosition) { + MOZ_ASSERT(IsOpen()); + + LOG_VERBOSE(("%p: Seeking to %" PRIu64, mStreamOwner.get(), aPosition)); + + return InvokeAsync( + mTaskQueue, __func__, + [selfHolder = fs::TargetPtrHolder(this), aPosition]() mutable { + QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), + CreateAndRejectBoolPromise); + + QM_TRY(MOZ_TO_RESULT(selfHolder->mStreamOwner->Seek(aPosition)), + CreateAndRejectBoolPromise); + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::Truncate(uint64_t aSize) { + MOZ_ASSERT(IsOpen()); + + return InvokeAsync( + mTaskQueue, __func__, + [selfHolder = fs::TargetPtrHolder(this), aSize]() mutable { + QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), + CreateAndRejectBoolPromise); + + QM_TRY(MOZ_TO_RESULT(selfHolder->mStreamOwner->Truncate(aSize)), + CreateAndRejectBoolPromise); + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +nsresult FileSystemWritableFileStream::EnsureStream() { + if (!mStreamOwner) { + QM_TRY_UNWRAP(MovingNotNull<nsCOMPtr<nsIRandomAccessStream>> stream, + DeserializeRandomAccessStream(mStreamParams), + NS_ERROR_FAILURE); + + mozilla::ipc::RandomAccessStreamParams streamParams( + std::move(mStreamParams)); + + mStreamOwner = MakeRefPtr<fs::FileSystemThreadSafeStreamOwner>( + this, std::move(stream)); + } + + return NS_OK; +} + +void FileSystemWritableFileStream::NoteFinishedCommand() { + MOZ_ASSERT(mCommandActive); + + mCommandActive = false; + + mFinishPromiseHolder.ResolveIfExists(true, __func__); +} + +RefPtr<BoolPromise> FileSystemWritableFileStream::Finish() { + if (!mCommandActive) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + return mFinishPromiseHolder.Ensure(__func__); +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) +NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableFileStreamUnderlyingSinkAlgorithms, + UnderlyingSinkAlgorithmsBase, mStream) + +// Step 3 of +// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream +already_AddRefed<Promise> +WritableFileStreamUnderlyingSinkAlgorithms::WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) { + return mStream->Write(aCx, aChunk, aRv); +} + +// Step 4 of +// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream +already_AddRefed<Promise> +WritableFileStreamUnderlyingSinkAlgorithms::CloseCallbackImpl( + JSContext* aCx, ErrorResult& aRv) { + RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!mStream->IsOpen()) { + promise->MaybeRejectWithTypeError("WritableFileStream closed"); + return promise.forget(); + } + + mStream->BeginClose()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](const BoolPromise::ResolveOrRejectValue& aValue) { + // Step 2.3. Return a promise resolved with undefined. + if (aValue.IsResolve()) { + promise->MaybeResolveWithUndefined(); + return; + } + promise->MaybeRejectWithAbortError( + "Internal error closing file stream"); + }); + + return promise.forget(); +} + +// Step 5 of +// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream +already_AddRefed<Promise> +WritableFileStreamUnderlyingSinkAlgorithms::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: + // Step 3.3. Return a promise resolved with undefined. + + RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!mStream->IsOpen()) { + promise->MaybeRejectWithTypeError("WritableFileStream closed"); + return promise.forget(); + } + + mStream->BeginAbort()->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](const BoolPromise::ResolveOrRejectValue& aValue) { + // Step 2.3. Return a promise resolved with undefined. + if (aValue.IsResolve()) { + promise->MaybeResolveWithUndefined(); + return; + } + promise->MaybeRejectWithAbortError( + "Internal error closing file stream"); + }); + + return promise.forget(); +} + +void WritableFileStreamUnderlyingSinkAlgorithms::ReleaseObjects() { + // WritableStream transitions to errored state whenever a rejected promise is + // returned. At the end of the transition, ReleaseObjects is called. + // Because there is no way to release the locks synchronously, + // we assume this has been initiated before the rejected promise is returned. + MOZ_ASSERT(!mStream->IsOpen()); +} + +} // namespace mozilla::dom |