/* -*- 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 "MutableBlobStorage.h" #include "MemoryBlobImpl.h" #include "mozilla/CheckedInt.h" #include "mozilla/Preferences.h" #include "mozilla/TaskQueue.h" #include "File.h" #include "nsAnonymousTemporaryFile.h" #include "nsNetCID.h" #include "nsProxyRelease.h" #include "WorkerPrivate.h" #define BLOB_MEMORY_TEMPORARY_FILE 1048576 namespace mozilla { namespace 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, Blob* aBlob, nsresult aRv) : mBlobStorage(aBlobStorage) , mCallback(aCallback) , mBlob(aBlob) , mRv(aRv) { MOZ_ASSERT(aBlobStorage); MOZ_ASSERT(aCallback); MOZ_ASSERT((NS_FAILED(aRv) && !aBlob) || (NS_SUCCEEDED(aRv) && aBlob)); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlobStorage); mCallback->BlobStoreCompleted(mBlobStorage, mBlob, mRv); mCallback = nullptr; mBlob = nullptr; return NS_OK; } private: ~BlobCreationDoneRunnable() { MOZ_ASSERT(mBlobStorage); // If something when wrong, we still have to release these objects in the // correct thread. NS_ProxyRelease(mBlobStorage->EventTarget(), mCallback.forget()); NS_ProxyRelease(mBlobStorage->EventTarget(), mBlob.forget()); } RefPtr mBlobStorage; RefPtr mCallback; RefPtr mBlob; nsresult mRv; }; // This runnable goes back to the main-thread and informs the BlobStorage about // the temporary file. class FileCreatedRunnable final : public Runnable { public: FileCreatedRunnable(MutableBlobStorage* aBlobStorage, PRFileDesc* aFD) : mBlobStorage(aBlobStorage) , mFD(aFD) { MOZ_ASSERT(aBlobStorage); MOZ_ASSERT(aFD); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); mBlobStorage->TemporaryFileCreated(mFD); mFD = nullptr; return NS_OK; } private: ~FileCreatedRunnable() { // If something when wrong, we still have to close the FileDescriptor. if (mFD) { PR_Close(mFD); } } RefPtr mBlobStorage; PRFileDesc* mFD; }; // This runnable creates the temporary file. When done, FileCreatedRunnable is // dispatched back to the main-thread. class CreateTemporaryFileRunnable final : public Runnable { public: explicit CreateTemporaryFileRunnable(MutableBlobStorage* aBlobStorage) : mBlobStorage(aBlobStorage) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(aBlobStorage); } NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(mBlobStorage); PRFileDesc* tempFD = nullptr; nsresult rv = NS_OpenAnonymousTemporaryFile(&tempFD); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_OK; } // The ownership of the tempFD is moved to the FileCreatedRunnable. return mBlobStorage->EventTarget()->Dispatch( new FileCreatedRunnable(mBlobStorage, tempFD), NS_DISPATCH_NORMAL); } private: RefPtr mBlobStorage; }; // Simple runnable to propagate the error to the BlobStorage. class ErrorPropagationRunnable final : public Runnable { public: ErrorPropagationRunnable(MutableBlobStorage* aBlobStorage, nsresult aRv) : mBlobStorage(aBlobStorage) , mRv(aRv) {} NS_IMETHOD Run() override { mBlobStorage->ErrorPropagated(mRv); return NS_OK; } private: RefPtr mBlobStorage; nsresult mRv; }; // This runnable moves a buffer to the IO thread and there, it writes it into // the temporary file. class WriteRunnable final : public Runnable { public: static WriteRunnable* CopyBuffer(MutableBlobStorage* aBlobStorage, PRFileDesc* aFD, const void* aData, uint32_t aLength) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aBlobStorage); MOZ_ASSERT(aFD); 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, aFD, data, aLength); } static WriteRunnable* AdoptBuffer(MutableBlobStorage* aBlobStorage, PRFileDesc* aFD, void* aData, uint32_t aLength) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aBlobStorage); MOZ_ASSERT(aFD); MOZ_ASSERT(aData); return new WriteRunnable(aBlobStorage, aFD, aData, aLength); } NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(mBlobStorage); int32_t written = PR_Write(mFD, mData, mLength); if (NS_WARN_IF(written < 0 || uint32_t(written) != mLength)) { return mBlobStorage->EventTarget()->Dispatch( new ErrorPropagationRunnable(mBlobStorage, NS_ERROR_FAILURE), NS_DISPATCH_NORMAL); } return NS_OK; } private: WriteRunnable(MutableBlobStorage* aBlobStorage, PRFileDesc* aFD, void* aData, uint32_t aLength) : mBlobStorage(aBlobStorage) , mFD(aFD) , mData(aData) , mLength(aLength) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlobStorage); MOZ_ASSERT(aFD); MOZ_ASSERT(aData); } ~WriteRunnable() { free(mData); } RefPtr mBlobStorage; PRFileDesc* mFD; 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) : mFD(aFD) {} NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread()); PR_Close(mFD); mFD = nullptr; return NS_OK; } private: ~CloseFileRunnable() { 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: CreateBlobRunnable(MutableBlobStorage* aBlobStorage, already_AddRefed aParent, const nsACString& aContentType, already_AddRefed aCallback) : mBlobStorage(aBlobStorage) , mParent(aParent) , mContentType(aContentType) , mCallback(aCallback) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aBlobStorage); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlobStorage); mBlobStorage->CreateBlobAndRespond(mParent.forget(), mContentType, mCallback.forget()); return NS_OK; } private: ~CreateBlobRunnable() { MOZ_ASSERT(mBlobStorage); // If something when wrong, we still have to release data in the correct // thread. NS_ProxyRelease(mBlobStorage->EventTarget(), mParent.forget()); NS_ProxyRelease(mBlobStorage->EventTarget(), mCallback.forget()); } RefPtr mBlobStorage; nsCOMPtr mParent; nsCString mContentType; RefPtr mCallback; }; // 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, nsISupports* aParent, const nsACString& aContentType, MutableBlobStorageCallback* aCallback) : mBlobStorage(aBlobStorage) , mParent(aParent) , mContentType(aContentType) , mCallback(aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mBlobStorage); MOZ_ASSERT(aCallback); } NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(mBlobStorage); RefPtr runnable = new CreateBlobRunnable(mBlobStorage, mParent.forget(), mContentType, mCallback.forget()); return mBlobStorage->EventTarget()->Dispatch(runnable, NS_DISPATCH_NORMAL); } private: ~LastRunnable() { MOZ_ASSERT(mBlobStorage); // If something when wrong, we still have to release data in the correct // thread. NS_ProxyRelease(mBlobStorage->EventTarget(), mParent.forget()); NS_ProxyRelease(mBlobStorage->EventTarget(), mCallback.forget()); } RefPtr mBlobStorage; nsCOMPtr mParent; nsCString mContentType; RefPtr mCallback; }; } // anonymous namespace MutableBlobStorage::MutableBlobStorage(MutableBlobStorageType aType, nsIEventTarget* aEventTarget) : mData(nullptr) , mDataLen(0) , mDataBufferLen(0) , mStorageState(aType == eOnlyInMemory ? eKeepInMemory : eInMemory) , mFD(nullptr) , mErrorResult(NS_OK) , mEventTarget(aEventTarget) { MOZ_ASSERT(NS_IsMainThread()); if (!mEventTarget) { mEventTarget = do_GetMainThread(); } MOZ_ASSERT(mEventTarget); } MutableBlobStorage::~MutableBlobStorage() { free(mData); if (mFD) { RefPtr runnable = new CloseFileRunnable(mFD); DispatchToIOThread(runnable.forget()); } if (mTaskQueue) { mTaskQueue->BeginShutdown(); } } uint64_t MutableBlobStorage::GetBlobWhenReady(nsISupports* aParent, const nsACString& aContentType, MutableBlobStorageCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aCallback); // GetBlob can be called just once. MOZ_ASSERT(mStorageState != eClosed); StorageState previousState = mStorageState; mStorageState = eClosed; if (previousState == eInTemporaryFile) { MOZ_ASSERT(mFD); if (NS_FAILED(mErrorResult)) { RefPtr runnable = new BlobCreationDoneRunnable(this, aCallback, nullptr, mErrorResult); EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); return 0; } // 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. RefPtr runnable = new LastRunnable(this, aParent, aContentType, aCallback); DispatchToIOThread(runnable.forget()); return mDataLen; } RefPtr 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 blob = Blob::Create(aParent, blobImpl); RefPtr runnable = new BlobCreationDoneRunnable(this, aCallback, blob, NS_OK); nsresult error = EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(error))) { return 0; } return mDataLen; } nsresult MutableBlobStorage::Append(const void* aData, uint32_t aLength) { MOZ_ASSERT(NS_IsMainThread()); 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(aLength)) { nsresult rv = MaybeCreateTemporaryFile(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // If we are already in the temporaryFile mode, we have to dispatch a // runnable. if (mStorageState == eInTemporaryFile) { MOZ_ASSERT(mFD); RefPtr runnable = WriteRunnable::CopyBuffer(this, mFD, aData, aLength); if (NS_WARN_IF(!runnable)) { return NS_ERROR_OUT_OF_MEMORY; } DispatchToIOThread(runnable.forget()); mDataLen += aLength; return NS_OK; } // By default, we store in memory. uint64_t offset = mDataLen; if (!ExpandBufferSize(aLength)) { return NS_ERROR_OUT_OF_MEMORY; } memcpy((char*)mData + offset, aData, aLength); return NS_OK; } bool MutableBlobStorage::ExpandBufferSize(uint64_t aSize) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mStorageState < eInTemporaryFile); if (mDataBufferLen >= mDataLen + aSize) { mDataLen += aSize; return true; } // Start at 1 or we'll loop forever. CheckedUint32 bufferLen = std::max(static_cast(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(uint64_t aSize) const { MOZ_ASSERT(mStorageState == eInMemory); CheckedUint32 bufferSize = mDataLen; bufferSize += aSize; if (!bufferSize.isValid()) { return false; } return bufferSize.value() >= Preferences::GetUint("dom.blob.memoryToTemporaryFile", BLOB_MEMORY_TEMPORARY_FILE); } nsresult MutableBlobStorage::MaybeCreateTemporaryFile() { if (XRE_IsParentProcess()) { RefPtr runnable = new CreateTemporaryFileRunnable(this); DispatchToIOThread(runnable.forget()); } else { RefPtr self(this); ContentChild::GetSingleton()-> AsyncOpenAnonymousTemporaryFile([self](PRFileDesc* prfile) { if (prfile) { // The ownership of the prfile is moved to the FileCreatedRunnable. self->EventTarget()->Dispatch( new FileCreatedRunnable(self, prfile), NS_DISPATCH_NORMAL); } }); } mStorageState = eWaitingForTemporaryFile; return NS_OK; } void MutableBlobStorage::TemporaryFileCreated(PRFileDesc* aFD) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mStorageState == eWaitingForTemporaryFile || mStorageState == eClosed); if (mStorageState == eClosed) { RefPtr runnable = new CloseFileRunnable(aFD); DispatchToIOThread(runnable.forget()); return; } mStorageState = eInTemporaryFile; mFD = aFD; RefPtr runnable = WriteRunnable::AdoptBuffer(this, mFD, mData, mDataLen); MOZ_ASSERT(runnable); mData = nullptr; DispatchToIOThread(runnable.forget()); } void MutableBlobStorage::CreateBlobAndRespond(already_AddRefed aParent, const nsACString& aContentType, already_AddRefed aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mStorageState == eClosed); MOZ_ASSERT(mFD); nsCOMPtr parent(aParent); RefPtr callback(aCallback); RefPtr blob = File::CreateTemporaryBlob(parent, mFD, 0, mDataLen, NS_ConvertUTF8toUTF16(aContentType)); callback->BlobStoreCompleted(this, blob, NS_OK); // ownership of this FD is moved to the BlobImpl. mFD = nullptr; } void MutableBlobStorage::ErrorPropagated(nsresult aRv) { MOZ_ASSERT(NS_IsMainThread()); mErrorResult = aRv; } void MutableBlobStorage::DispatchToIOThread(already_AddRefed aRunnable) { if (!mTaskQueue) { nsCOMPtr target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); MOZ_ASSERT(target); mTaskQueue = new TaskQueue(target.forget()); } nsCOMPtr runnable(aRunnable); mTaskQueue->Dispatch(runnable.forget()); } } // dom namespace } // mozilla namespace