diff options
Diffstat (limited to 'dom/file/MutableBlobStorage.cpp')
-rw-r--r-- | dom/file/MutableBlobStorage.cpp | 667 |
1 files changed, 667 insertions, 0 deletions
diff --git a/dom/file/MutableBlobStorage.cpp b/dom/file/MutableBlobStorage.cpp new file mode 100644 index 0000000000..3bb1a4f169 --- /dev/null +++ b/dom/file/MutableBlobStorage.cpp @@ -0,0 +1,667 @@ +/* -*- 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 "EmptyBlobImpl.h" +#include "MutableBlobStorage.h" +#include "MemoryBlobImpl.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/TemporaryIPCBlobChild.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/TaskQueue.h" +#include "File.h" +#include "nsAnonymousTemporaryFile.h" +#include "nsNetCID.h" +#include "nsProxyRelease.h" + +#define BLOB_MEMORY_TEMPORARY_FILE 1048576 + +namespace mozilla::dom { + +namespace { + +// This class uses the callback to inform when the Blob is created or when the +// error must be propagated. +class BlobCreationDoneRunnable final : public Runnable { + public: + BlobCreationDoneRunnable(MutableBlobStorage* aBlobStorage, + MutableBlobStorageCallback* aCallback, + BlobImpl* aBlobImpl, nsresult aRv) + : Runnable("dom::BlobCreationDoneRunnable"), + mBlobStorage(aBlobStorage), + mCallback(aCallback), + mBlobImpl(aBlobImpl), + mRv(aRv) { + MOZ_ASSERT(aBlobStorage); + MOZ_ASSERT(aCallback); + MOZ_ASSERT((NS_FAILED(aRv) && !aBlobImpl) || + (NS_SUCCEEDED(aRv) && aBlobImpl)); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + mCallback->BlobStoreCompleted(mBlobStorage, mBlobImpl, mRv); + mCallback = nullptr; + mBlobImpl = nullptr; + return NS_OK; + } + + private: + ~BlobCreationDoneRunnable() override { + MOZ_ASSERT(mBlobStorage); + // If something when wrong, we still have to release these objects in the + // correct thread. + NS_ProxyRelease("BlobCreationDoneRunnable::mCallback", + mBlobStorage->EventTarget(), mCallback.forget()); + } + + RefPtr<MutableBlobStorage> mBlobStorage; + RefPtr<MutableBlobStorageCallback> mCallback; + RefPtr<BlobImpl> mBlobImpl; + nsresult mRv; +}; + +// Simple runnable to propagate the error to the BlobStorage. +class ErrorPropagationRunnable final : public Runnable { + public: + ErrorPropagationRunnable(MutableBlobStorage* aBlobStorage, nsresult aRv) + : Runnable("dom::ErrorPropagationRunnable"), + mBlobStorage(aBlobStorage), + mRv(aRv) {} + + NS_IMETHOD + Run() override { + mBlobStorage->ErrorPropagated(mRv); + return NS_OK; + } + + private: + RefPtr<MutableBlobStorage> mBlobStorage; + nsresult mRv; +}; + +// This runnable moves a buffer to the IO thread and there, it writes it into +// the temporary file, if its File Descriptor has not been already closed. +class WriteRunnable final : public Runnable { + public: + static WriteRunnable* CopyBuffer(MutableBlobStorage* aBlobStorage, + const void* aData, uint32_t aLength) { + MOZ_ASSERT(aBlobStorage); + MOZ_ASSERT(aData); + + // We have to take a copy of this buffer. + void* data = malloc(aLength); + if (!data) { + return nullptr; + } + + memcpy((char*)data, aData, aLength); + return new WriteRunnable(aBlobStorage, data, aLength); + } + + static WriteRunnable* AdoptBuffer(MutableBlobStorage* aBlobStorage, + void* aData, uint32_t aLength) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBlobStorage); + MOZ_ASSERT(aData); + + return new WriteRunnable(aBlobStorage, aData, aLength); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + + PRFileDesc* fd = mBlobStorage->GetFD(); + if (!fd) { + // The file descriptor has been closed in the meantime. + return NS_OK; + } + + int32_t written = PR_Write(fd, mData, mLength); + if (NS_WARN_IF(written < 0 || uint32_t(written) != mLength)) { + mBlobStorage->CloseFD(); + return mBlobStorage->EventTarget()->Dispatch( + new ErrorPropagationRunnable(mBlobStorage, NS_ERROR_FAILURE), + NS_DISPATCH_NORMAL); + } + + return NS_OK; + } + + private: + WriteRunnable(MutableBlobStorage* aBlobStorage, void* aData, uint32_t aLength) + : Runnable("dom::WriteRunnable"), + mBlobStorage(aBlobStorage), + mData(aData), + mLength(aLength) { + MOZ_ASSERT(mBlobStorage); + MOZ_ASSERT(aData); + } + + ~WriteRunnable() override { free(mData); } + + RefPtr<MutableBlobStorage> mBlobStorage; + void* mData; + uint32_t mLength; +}; + +// This runnable closes the FD in case something goes wrong or the temporary +// file is not needed anymore. +class CloseFileRunnable final : public Runnable { + public: + explicit CloseFileRunnable(PRFileDesc* aFD) + : Runnable("dom::CloseFileRunnable"), mFD(aFD) {} + + NS_IMETHOD + Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + PR_Close(mFD); + mFD = nullptr; + return NS_OK; + } + + private: + ~CloseFileRunnable() override { + if (mFD) { + PR_Close(mFD); + } + } + + PRFileDesc* mFD; +}; + +// This runnable is dispatched to the main-thread from the IO thread and its +// task is to create the blob and inform the callback. +class CreateBlobRunnable final : public Runnable, + public TemporaryIPCBlobChildCallback { + public: + // We need to always declare refcounting because + // TemporaryIPCBlobChildCallback has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + CreateBlobRunnable(MutableBlobStorage* aBlobStorage, + const nsACString& aContentType, + already_AddRefed<MutableBlobStorageCallback> aCallback) + : Runnable("dom::CreateBlobRunnable"), + mBlobStorage(aBlobStorage), + mContentType(aContentType), + mCallback(aCallback) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aBlobStorage); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + mBlobStorage->AskForBlob(this, mContentType); + return NS_OK; + } + + void OperationSucceeded(BlobImpl* aBlobImpl) override { + RefPtr<MutableBlobStorageCallback> callback(std::move(mCallback)); + callback->BlobStoreCompleted(mBlobStorage, aBlobImpl, NS_OK); + } + + void OperationFailed(nsresult aRv) override { + RefPtr<MutableBlobStorageCallback> callback(std::move(mCallback)); + callback->BlobStoreCompleted(mBlobStorage, nullptr, aRv); + } + + private: + ~CreateBlobRunnable() override { + MOZ_ASSERT(mBlobStorage); + // If something when wrong, we still have to release data in the correct + // thread. + NS_ProxyRelease("CreateBlobRunnable::mCallback", + mBlobStorage->EventTarget(), mCallback.forget()); + } + + RefPtr<MutableBlobStorage> mBlobStorage; + nsCString mContentType; + RefPtr<MutableBlobStorageCallback> mCallback; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(CreateBlobRunnable, Runnable) + +// This task is used to know when the writing is completed. From the IO thread +// it dispatches a CreateBlobRunnable to the main-thread. +class LastRunnable final : public Runnable { + public: + LastRunnable(MutableBlobStorage* aBlobStorage, const nsACString& aContentType, + MutableBlobStorageCallback* aCallback) + : Runnable("dom::LastRunnable"), + mBlobStorage(aBlobStorage), + mContentType(aContentType), + mCallback(aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBlobStorage); + MOZ_ASSERT(aCallback); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + + RefPtr<Runnable> runnable = + new CreateBlobRunnable(mBlobStorage, mContentType, mCallback.forget()); + return mBlobStorage->EventTarget()->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + + private: + ~LastRunnable() override { + MOZ_ASSERT(mBlobStorage); + // If something when wrong, we still have to release data in the correct + // thread. + NS_ProxyRelease("LastRunnable::mCallback", mBlobStorage->EventTarget(), + mCallback.forget()); + } + + RefPtr<MutableBlobStorage> mBlobStorage; + nsCString mContentType; + RefPtr<MutableBlobStorageCallback> mCallback; +}; + +} // anonymous namespace + +MutableBlobStorage::MutableBlobStorage(MutableBlobStorageType aType, + nsIEventTarget* aEventTarget, + uint32_t aMaxMemory) + : mMutex("MutableBlobStorage::mMutex"), + mData(nullptr), + mDataLen(0), + mDataBufferLen(0), + mStorageState(aType == eOnlyInMemory ? eKeepInMemory : eInMemory), + mFD(nullptr), + mErrorResult(NS_OK), + mEventTarget(aEventTarget), + mMaxMemory(aMaxMemory) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mEventTarget) { + mEventTarget = GetMainThreadEventTarget(); + } + + if (aMaxMemory == 0 && aType == eCouldBeInTemporaryFile) { + mMaxMemory = Preferences::GetUint("dom.blob.memoryToTemporaryFile", + BLOB_MEMORY_TEMPORARY_FILE); + } + + MOZ_ASSERT(mEventTarget); +} + +MutableBlobStorage::~MutableBlobStorage() { + free(mData); + + if (mFD) { + RefPtr<Runnable> runnable = new CloseFileRunnable(mFD); + Unused << DispatchToIOThread(runnable.forget()); + } + + if (mTaskQueue) { + mTaskQueue->BeginShutdown(); + } + + if (mActor) { + NS_ProxyRelease("MutableBlobStorage::mActor", EventTarget(), + mActor.forget()); + } +} + +void MutableBlobStorage::GetBlobImplWhenReady( + const nsACString& aContentType, MutableBlobStorageCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCallback); + + MutexAutoLock lock(mMutex); + + // GetBlob can be called just once. + MOZ_ASSERT(mStorageState != eClosed); + StorageState previousState = mStorageState; + mStorageState = eClosed; + + if (previousState == eInTemporaryFile) { + if (NS_FAILED(mErrorResult)) { + MOZ_ASSERT(!mActor); + + RefPtr<Runnable> runnable = + new BlobCreationDoneRunnable(this, aCallback, nullptr, mErrorResult); + EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + return; + } + + MOZ_ASSERT(mActor); + + // We want to wait until all the WriteRunnable are completed. The way we do + // this is to go to the I/O thread and then we come back: the runnables are + // executed in order and this LastRunnable will be... the last one. + // This Runnable will also close the FD on the I/O thread. + RefPtr<Runnable> runnable = new LastRunnable(this, aContentType, aCallback); + + // If the dispatching fails, we are shutting down and it's fine to do not + // run the callback. + Unused << DispatchToIOThread(runnable.forget()); + return; + } + + // If we are waiting for the temporary file, it's better to wait... + if (previousState == eWaitingForTemporaryFile) { + mPendingContentType = aContentType; + mPendingCallback = aCallback; + return; + } + + RefPtr<BlobImpl> blobImpl; + + if (mData) { + blobImpl = new MemoryBlobImpl(mData, mDataLen, + NS_ConvertUTF8toUTF16(aContentType)); + + mData = nullptr; // The MemoryBlobImpl takes ownership of the buffer + mDataLen = 0; + mDataBufferLen = 0; + } else { + blobImpl = new EmptyBlobImpl(NS_ConvertUTF8toUTF16(aContentType)); + } + + RefPtr<BlobCreationDoneRunnable> runnable = + new BlobCreationDoneRunnable(this, aCallback, blobImpl, NS_OK); + + nsresult error = + EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(error))) { + return; + } +} + +nsresult MutableBlobStorage::Append(const void* aData, uint32_t aLength) { + // This method can be called on any thread. + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mStorageState != eClosed); + NS_ENSURE_ARG_POINTER(aData); + + if (!aLength) { + return NS_OK; + } + + // If eInMemory is the current Storage state, we could maybe migrate to + // a temporary file. + if (mStorageState == eInMemory && ShouldBeTemporaryStorage(lock, aLength) && + !MaybeCreateTemporaryFile(lock)) { + return NS_ERROR_FAILURE; + } + + // If we are already in the temporaryFile mode, we have to dispatch a + // runnable. + if (mStorageState == eInTemporaryFile) { + // If a previous operation failed, let's return that error now. + if (NS_FAILED(mErrorResult)) { + return mErrorResult; + } + + RefPtr<WriteRunnable> runnable = + WriteRunnable::CopyBuffer(this, aData, aLength); + if (NS_WARN_IF(!runnable)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsresult rv = DispatchToIOThread(runnable.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mDataLen += aLength; + return NS_OK; + } + + // By default, we store in memory. + + uint64_t offset = mDataLen; + + if (!ExpandBufferSize(lock, aLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy((char*)mData + offset, aData, aLength); + return NS_OK; +} + +bool MutableBlobStorage::ExpandBufferSize(const MutexAutoLock& aProofOfLock, + uint64_t aSize) { + MOZ_ASSERT(mStorageState < eInTemporaryFile); + + if (mDataBufferLen >= mDataLen + aSize) { + mDataLen += aSize; + return true; + } + + // Start at 1 or we'll loop forever. + CheckedUint32 bufferLen = + std::max<uint32_t>(static_cast<uint32_t>(mDataBufferLen), 1); + while (bufferLen.isValid() && bufferLen.value() < mDataLen + aSize) { + bufferLen *= 2; + } + + if (!bufferLen.isValid()) { + return false; + } + + void* data = realloc(mData, bufferLen.value()); + if (!data) { + return false; + } + + mData = data; + mDataBufferLen = bufferLen.value(); + mDataLen += aSize; + return true; +} + +bool MutableBlobStorage::ShouldBeTemporaryStorage( + const MutexAutoLock& aProofOfLock, uint64_t aSize) const { + MOZ_ASSERT(mStorageState == eInMemory); + + CheckedUint32 bufferSize = mDataLen; + bufferSize += aSize; + + if (!bufferSize.isValid()) { + return false; + } + + return bufferSize.value() >= mMaxMemory; +} + +bool MutableBlobStorage::MaybeCreateTemporaryFile( + const MutexAutoLock& aProofOfLock) { + mStorageState = eWaitingForTemporaryFile; + + if (!NS_IsMainThread()) { + RefPtr<MutableBlobStorage> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "MutableBlobStorage::MaybeCreateTemporaryFile", [self]() { + MutexAutoLock lock(self->mMutex); + self->MaybeCreateTemporaryFileOnMainThread(lock); + if (!self->mActor) { + self->ErrorPropagated(NS_ERROR_FAILURE); + } + }); + EventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL); + return true; + } + + MaybeCreateTemporaryFileOnMainThread(aProofOfLock); + return !!mActor; +} + +void MutableBlobStorage::MaybeCreateTemporaryFileOnMainThread( + const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mActor); + + mozilla::ipc::PBackgroundChild* actorChild = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + return; + } + + mActor = new TemporaryIPCBlobChild(this); + actorChild->SendPTemporaryIPCBlobConstructor(mActor); + + // We need manually to increase the reference for this actor because the + // IPC allocator method is not triggered. The Release() is called by IPDL + // when the actor is deleted. + mActor.get()->AddRef(); + + // The actor will call us when the FileDescriptor is received. +} + +void MutableBlobStorage::TemporaryFileCreated(PRFileDesc* aFD) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mStorageState == eWaitingForTemporaryFile || + mStorageState == eClosed); + MOZ_ASSERT_IF(mPendingCallback, mStorageState == eClosed); + MOZ_ASSERT(mActor); + MOZ_ASSERT(aFD); + + // If the object has been already closed and we don't need to execute a + // callback, we need just to close the file descriptor in the correct thread. + if (mStorageState == eClosed && !mPendingCallback) { + RefPtr<Runnable> runnable = new CloseFileRunnable(aFD); + + // If this dispatching fails, CloseFileRunnable will close the FD in the + // DTOR on the current thread. + Unused << DispatchToIOThread(runnable.forget()); + + // Let's inform the parent that we have nothing else to do. + mActor->SendOperationFailed(); + mActor = nullptr; + return; + } + + // If we still receiving data, we can proceed in temporary-file mode. + if (mStorageState == eWaitingForTemporaryFile) { + mStorageState = eInTemporaryFile; + } + + mFD = aFD; + MOZ_ASSERT(NS_SUCCEEDED(mErrorResult)); + + // This runnable takes the ownership of mData and it will write this buffer + // into the temporary file. + RefPtr<WriteRunnable> runnable = + WriteRunnable::AdoptBuffer(this, mData, mDataLen); + MOZ_ASSERT(runnable); + + mData = nullptr; + + nsresult rv = DispatchToIOThread(runnable.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Shutting down, we cannot continue. + return; + } + + // If we are closed, it means that GetBlobImplWhenReady() has been called when + // we were already waiting for a temporary file-descriptor. Finally we are + // here, AdoptBuffer runnable is going to write the current buffer into this + // file. After that, there is nothing else to write, and we dispatch + // LastRunnable which ends up calling mPendingCallback via CreateBlobRunnable. + if (mStorageState == eClosed) { + MOZ_ASSERT(mPendingCallback); + + RefPtr<Runnable> runnable = + new LastRunnable(this, mPendingContentType, mPendingCallback); + Unused << DispatchToIOThread(runnable.forget()); + + mPendingCallback = nullptr; + } +} + +void MutableBlobStorage::AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mStorageState == eClosed); + MOZ_ASSERT(mFD); + MOZ_ASSERT(mActor); + MOZ_ASSERT(aCallback); + + // Let's pass the FileDescriptor to the parent actor in order to keep the file + // locked on windows. + mActor->AskForBlob(aCallback, aContentType, mFD); + + // The previous operation has duplicated the file descriptor. Now we can close + // mFD. The parent will take care of closing the duplicated file descriptor on + // its side. + RefPtr<Runnable> runnable = new CloseFileRunnable(mFD); + Unused << DispatchToIOThread(runnable.forget()); + + mFD = nullptr; + mActor = nullptr; +} + +void MutableBlobStorage::ErrorPropagated(nsresult aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + mErrorResult = aRv; + + if (mActor) { + mActor->SendOperationFailed(); + mActor = nullptr; + } +} + +nsresult MutableBlobStorage::DispatchToIOThread( + already_AddRefed<nsIRunnable> aRunnable) { + if (!mTaskQueue) { + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + mTaskQueue = TaskQueue::Create(target.forget(), "BlobStorage"); + } + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + nsresult rv = mTaskQueue->Dispatch(runnable.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +size_t MutableBlobStorage::SizeOfCurrentMemoryBuffer() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mMutex); + return mStorageState < eInTemporaryFile ? mDataLen : 0; +} + +PRFileDesc* MutableBlobStorage::GetFD() { + MOZ_ASSERT(!NS_IsMainThread()); + MutexAutoLock lock(mMutex); + return mFD; +} + +void MutableBlobStorage::CloseFD() { + MOZ_ASSERT(!NS_IsMainThread()); + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mFD); + + PR_Close(mFD); + mFD = nullptr; +} + +} // namespace mozilla::dom |