/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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/MutableBlobStorage.h" #include "mozilla/CheckedInt.h" #include "mozilla/dom/File.h" #include "mozilla/TaskQueue.h" #include "nsAnonymousTemporaryFile.h" #include "nsNetCID.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()); mCallback->BlobStoreCompleted(mBlobStorage, mBlob, mRv); mCallback = nullptr; mBlob = nullptr; return NS_OK; } private: ~BlobCreationDoneRunnable() { // If something when wrong, we still have to release these objects in the // correct thread. NS_ReleaseOnMainThread(mCallback.forget()); NS_ReleaseOnMainThread(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(!NS_IsMainThread()); 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(aBlobStorage); } NS_IMETHOD Run() override { MOZ_ASSERT(!NS_IsMainThread()); PRFileDesc* tempFD = nullptr; nsresult rv = NS_OpenAnonymousTemporaryFile(&tempFD); if (NS_WARN_IF(NS_FAILED(rv))) { // In sandboxed context we are not allowed to create temporary files, but // this doesn't mean that BlobStorage should fail. We can continue to // store data in memory. We don't change the storageType so that we don't // try to create a temporary file again. return NS_OK; } // The ownership of the tempFD is moved to the FileCreatedRunnable. return NS_DispatchToMainThread(new FileCreatedRunnable(mBlobStorage, tempFD)); } 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()); int32_t written = PR_Write(mFD, mData, mLength); if (NS_WARN_IF(written < 0 || uint32_t(written) != mLength)) { return NS_DispatchToMainThread( new ErrorPropagationRunnable(mBlobStorage, NS_ERROR_FAILURE)); } 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()); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); mBlobStorage->CreateBlobAndRespond(mParent.forget(), mContentType, mCallback.forget()); return NS_OK; } private: ~CreateBlobRunnable() { // If something when wrong, we still have to release data in the correct // thread. NS_ReleaseOnMainThread(mParent.forget()); NS_ReleaseOnMainThread(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()); RefPtr runnable = new CreateBlobRunnable(mBlobStorage, mParent.forget(), mContentType, mCallback.forget()); return NS_DispatchToMainThread(runnable); } private: ~LastRunnable() { // If something when wrong, we still have to release data in the correct // thread. NS_ReleaseOnMainThread(mParent.forget()); NS_ReleaseOnMainThread(mCallback.forget()); } RefPtr mBlobStorage; nsCOMPtr mParent; nsCString mContentType; RefPtr mCallback; }; } // anonymous namespace MutableBlobStorage::MutableBlobStorage(MutableBlobStorageType aType) : mData(nullptr) , mDataLen(0) , mDataBufferLen(0) , mStorageState(aType == eOnlyInMemory ? eKeepInMemory : eInMemory) , mFD(nullptr) , mErrorResult(NS_OK) { MOZ_ASSERT(NS_IsMainThread()); } 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); NS_DispatchToMainThread(runnable.forget()); 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; } // If we are waiting for the temporary file, it's better to wait... if (previousState == eWaitingForTemporaryFile) { mPendingParent = aParent; mPendingContentType = aContentType; mPendingCallback = aCallback; return mDataLen; } RefPtr blobImpl; if (mData) { blobImpl = new BlobImplMemory(mData, mDataLen, NS_ConvertUTF8toUTF16(aContentType)); mData = nullptr; // The BlobImplMemory 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 = NS_DispatchToMainThread(runnable); 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() { RefPtr runnable = new CreateTemporaryFileRunnable(this); DispatchToIOThread(runnable.forget()); mStorageState = eWaitingForTemporaryFile; return NS_OK; } void MutableBlobStorage::TemporaryFileCreated(PRFileDesc* aFD) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mStorageState == eWaitingForTemporaryFile || mStorageState == eClosed); MOZ_ASSERT_IF(mPendingCallback, mStorageState == eClosed); // 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 = new CloseFileRunnable(aFD); DispatchToIOThread(runnable.forget()); return; } // If we still receiving data, we can proceed in temporary-file mode. if (mStorageState == eWaitingForTemporaryFile) { mStorageState = eInTemporaryFile; } mFD = aFD; // This runnable takes the ownership of mData and it will write this buffer // into the temporary file. RefPtr runnable = WriteRunnable::AdoptBuffer(this, mFD, mData, mDataLen); MOZ_ASSERT(runnable); mData = nullptr; DispatchToIOThread(runnable.forget()); // If we are closed, it means that GetBlobWhenReady() 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 = new LastRunnable(this, mPendingParent, mPendingContentType, mPendingCallback); DispatchToIOThread(runnable.forget()); mPendingParent = nullptr; mPendingCallback = nullptr; } } 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