diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/indexedDB | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/indexedDB')
406 files changed, 85077 insertions, 0 deletions
diff --git a/dom/indexedDB/ActorsChild.cpp b/dom/indexedDB/ActorsChild.cpp new file mode 100644 index 000000000..3e8f97348 --- /dev/null +++ b/dom/indexedDB/ActorsChild.cpp @@ -0,0 +1,3588 @@ +/* -*- 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 "ActorsChild.h" + +#include "BackgroundChildImpl.h" +#include "IDBDatabase.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBMutableFile.h" +#include "IDBObjectStore.h" +#include "IDBMutableFile.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Maybe.h" +#include "mozilla/TypeTraits.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/TabChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileChild.h" +#include "mozilla/dom/indexedDB/PIndexedDBPermissionRequestChild.h" +#include "mozilla/dom/ipc/BlobChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIBFCacheEntry.h" +#include "nsIDocument.h" +#include "nsIDOMEvent.h" +#include "nsIEventTarget.h" +#include "nsIFileStreams.h" +#include "nsNetCID.h" +#include "nsPIDOMWindow.h" +#include "nsThreadUtils.h" +#include "nsTraceRefcnt.h" +#include "PermissionRequestBase.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" + +#ifdef DEBUG +#include "IndexedDatabaseManager.h" +#endif + +#define GC_ON_IPC_MESSAGES 0 + +#if defined(DEBUG) || GC_ON_IPC_MESSAGES + +#include "js/GCAPI.h" +#include "nsJSEnvironment.h" + +#define BUILD_GC_ON_IPC_MESSAGES + +#endif // DEBUG || GC_ON_IPC_MESSAGES + +namespace mozilla { + +using ipc::PrincipalInfo; + +namespace dom { + +using namespace workers; + +namespace indexedDB { + +/******************************************************************************* + * ThreadLocal + ******************************************************************************/ + +ThreadLocal::ThreadLocal(const nsID& aBackgroundChildLoggingId) + : mLoggingInfo(aBackgroundChildLoggingId, 1, -1, 1) + , mCurrentTransaction(0) +#ifdef DEBUG + , mOwningThread(PR_GetCurrentThread()) +#endif +{ + MOZ_ASSERT(mOwningThread); + + MOZ_COUNT_CTOR(mozilla::dom::indexedDB::ThreadLocal); + + // NSID_LENGTH counts the null terminator, SetLength() does not. + mLoggingIdString.SetLength(NSID_LENGTH - 1); + + aBackgroundChildLoggingId.ToProvidedString( + *reinterpret_cast<char(*)[NSID_LENGTH]>(mLoggingIdString.BeginWriting())); +} + +ThreadLocal::~ThreadLocal() +{ + MOZ_COUNT_DTOR(mozilla::dom::indexedDB::ThreadLocal); +} + +#ifdef DEBUG + +void +ThreadLocal::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread); +} + +#endif // DEBUG + +/******************************************************************************* + * Helpers + ******************************************************************************/ + +namespace { + +void +MaybeCollectGarbageOnIPCMessage() +{ +#ifdef BUILD_GC_ON_IPC_MESSAGES + static const bool kCollectGarbageOnIPCMessages = +#if GC_ON_IPC_MESSAGES + true; +#else + false; +#endif // GC_ON_IPC_MESSAGES + + if (!kCollectGarbageOnIPCMessages) { + return; + } + + static bool haveWarnedAboutGC = false; + static bool haveWarnedAboutNonMainThread = false; + + if (!haveWarnedAboutGC) { + haveWarnedAboutGC = true; + NS_WARNING("IndexedDB child actor GC debugging enabled!"); + } + + if (!NS_IsMainThread()) { + if (!haveWarnedAboutNonMainThread) { + haveWarnedAboutNonMainThread = true; + NS_WARNING("Don't know how to GC on a non-main thread yet."); + } + return; + } + + nsJSContext::GarbageCollectNow(JS::gcreason::DOM_IPC); + nsJSContext::CycleCollectNow(); +#endif // BUILD_GC_ON_IPC_MESSAGES +} + +class MOZ_STACK_CLASS AutoSetCurrentTransaction final +{ + typedef mozilla::ipc::BackgroundChildImpl BackgroundChildImpl; + + IDBTransaction* const mTransaction; + IDBTransaction* mPreviousTransaction; + ThreadLocal* mThreadLocal; + +public: + explicit AutoSetCurrentTransaction(IDBTransaction* aTransaction) + : mTransaction(aTransaction) + , mPreviousTransaction(nullptr) + , mThreadLocal(nullptr) + { + if (aTransaction) { + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + // Hang onto this for resetting later. + mThreadLocal = threadLocal->mIndexedDBThreadLocal; + MOZ_ASSERT(mThreadLocal); + + // Save the current value. + mPreviousTransaction = mThreadLocal->GetCurrentTransaction(); + + // Set the new value. + mThreadLocal->SetCurrentTransaction(aTransaction); + } + } + + ~AutoSetCurrentTransaction() + { + MOZ_ASSERT_IF(mThreadLocal, mTransaction); + MOZ_ASSERT_IF(mThreadLocal, + mThreadLocal->GetCurrentTransaction() == mTransaction); + + if (mThreadLocal) { + // Reset old value. + mThreadLocal->SetCurrentTransaction(mPreviousTransaction); + } + } + + IDBTransaction* + Transaction() const + { + return mTransaction; + } +}; + +class MOZ_STACK_CLASS ResultHelper final + : public IDBRequest::ResultCallback +{ + IDBRequest* mRequest; + AutoSetCurrentTransaction mAutoTransaction; + + union + { + IDBDatabase* mDatabase; + IDBCursor* mCursor; + IDBMutableFile* mMutableFile; + StructuredCloneReadInfo* mStructuredClone; + const nsTArray<StructuredCloneReadInfo>* mStructuredCloneArray; + const Key* mKey; + const nsTArray<Key>* mKeyArray; + const JS::Value* mJSVal; + const JS::Handle<JS::Value>* mJSValHandle; + } mResult; + + enum + { + ResultTypeDatabase, + ResultTypeCursor, + ResultTypeMutableFile, + ResultTypeStructuredClone, + ResultTypeStructuredCloneArray, + ResultTypeKey, + ResultTypeKeyArray, + ResultTypeJSVal, + ResultTypeJSValHandle, + } mResultType; + +public: + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + IDBDatabase* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeDatabase) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aResult); + + mResult.mDatabase = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + IDBCursor* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeCursor) + { + MOZ_ASSERT(aRequest); + + mResult.mCursor = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + IDBMutableFile* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeMutableFile) + { + MOZ_ASSERT(aRequest); + + mResult.mMutableFile = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + StructuredCloneReadInfo* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeStructuredClone) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aResult); + + mResult.mStructuredClone = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + const nsTArray<StructuredCloneReadInfo>* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeStructuredCloneArray) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aResult); + + mResult.mStructuredCloneArray = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + const Key* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeKey) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aResult); + + mResult.mKey = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + const nsTArray<Key>* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeKeyArray) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aResult); + + mResult.mKeyArray = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + const JS::Value* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeJSVal) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(!aResult->isGCThing()); + + mResult.mJSVal = aResult; + } + + ResultHelper(IDBRequest* aRequest, + IDBTransaction* aTransaction, + const JS::Handle<JS::Value>* aResult) + : mRequest(aRequest) + , mAutoTransaction(aTransaction) + , mResultType(ResultTypeJSValHandle) + { + MOZ_ASSERT(aRequest); + + mResult.mJSValHandle = aResult; + } + + IDBRequest* + Request() const + { + return mRequest; + } + + IDBTransaction* + Transaction() const + { + return mAutoTransaction.Transaction(); + } + + virtual nsresult + GetResult(JSContext* aCx, JS::MutableHandle<JS::Value> aResult) override + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(mRequest); + + switch (mResultType) { + case ResultTypeDatabase: + return GetResult(aCx, mResult.mDatabase, aResult); + + case ResultTypeCursor: + return GetResult(aCx, mResult.mCursor, aResult); + + case ResultTypeMutableFile: + return GetResult(aCx, mResult.mMutableFile, aResult); + + case ResultTypeStructuredClone: + return GetResult(aCx, mResult.mStructuredClone, aResult); + + case ResultTypeStructuredCloneArray: + return GetResult(aCx, mResult.mStructuredCloneArray, aResult); + + case ResultTypeKey: + return GetResult(aCx, mResult.mKey, aResult); + + case ResultTypeKeyArray: + return GetResult(aCx, mResult.mKeyArray, aResult); + + case ResultTypeJSVal: + aResult.set(*mResult.mJSVal); + return NS_OK; + + case ResultTypeJSValHandle: + aResult.set(*mResult.mJSValHandle); + return NS_OK; + + default: + MOZ_CRASH("Unknown result type!"); + } + + MOZ_CRASH("Should never get here!"); + } + +private: + template <class T> + typename EnableIf<IsSame<T, IDBDatabase>::value || + IsSame<T, IDBCursor>::value || + IsSame<T, IDBMutableFile>::value, + nsresult>::Type + GetResult(JSContext* aCx, + T* aDOMObject, + JS::MutableHandle<JS::Value> aResult) + { + if (!aDOMObject) { + aResult.setNull(); + return NS_OK; + } + + bool ok = GetOrCreateDOMReflector(aCx, aDOMObject, aResult); + if (NS_WARN_IF(!ok)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; + } + + nsresult + GetResult(JSContext* aCx, + StructuredCloneReadInfo* aCloneInfo, + JS::MutableHandle<JS::Value> aResult) + { + bool ok = IDBObjectStore::DeserializeValue(aCx, *aCloneInfo, aResult); + + if (NS_WARN_IF(!ok)) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + return NS_OK; + } + + nsresult + GetResult(JSContext* aCx, + const nsTArray<StructuredCloneReadInfo>* aCloneInfos, + JS::MutableHandle<JS::Value> aResult) + { + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, 0)); + if (NS_WARN_IF(!array)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (!aCloneInfos->IsEmpty()) { + const uint32_t count = aCloneInfos->Length(); + + if (NS_WARN_IF(!JS_SetArrayLength(aCx, array, count))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + for (uint32_t index = 0; index < count; index++) { + auto& cloneInfo = + const_cast<StructuredCloneReadInfo&>(aCloneInfos->ElementAt(index)); + + JS::Rooted<JS::Value> value(aCx); + + nsresult rv = GetResult(aCx, &cloneInfo, &value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!JS_DefineElement(aCx, array, index, value, + JSPROP_ENUMERATE))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + } + + aResult.setObject(*array); + return NS_OK; + } + + nsresult + GetResult(JSContext* aCx, + const Key* aKey, + JS::MutableHandle<JS::Value> aResult) + { + nsresult rv = aKey->ToJSVal(aCx, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + nsresult + GetResult(JSContext* aCx, + const nsTArray<Key>* aKeys, + JS::MutableHandle<JS::Value> aResult) + { + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, 0)); + if (NS_WARN_IF(!array)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (!aKeys->IsEmpty()) { + const uint32_t count = aKeys->Length(); + + if (NS_WARN_IF(!JS_SetArrayLength(aCx, array, count))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + for (uint32_t index = 0; index < count; index++) { + const Key& key = aKeys->ElementAt(index); + MOZ_ASSERT(!key.IsUnset()); + + JS::Rooted<JS::Value> value(aCx); + + nsresult rv = GetResult(aCx, &key, &value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!JS_DefineElement(aCx, array, index, value, + JSPROP_ENUMERATE))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + } + + aResult.setObject(*array); + return NS_OK; + } +}; + +class PermissionRequestMainProcessHelper final + : public PermissionRequestBase +{ + BackgroundFactoryRequestChild* mActor; + RefPtr<IDBFactory> mFactory; + +public: + PermissionRequestMainProcessHelper(BackgroundFactoryRequestChild* aActor, + IDBFactory* aFactory, + Element* aOwnerElement, + nsIPrincipal* aPrincipal) + : PermissionRequestBase(aOwnerElement, aPrincipal) + , mActor(aActor) + , mFactory(aFactory) + { + MOZ_ASSERT(aActor); + MOZ_ASSERT(aFactory); + aActor->AssertIsOnOwningThread(); + } + +protected: + ~PermissionRequestMainProcessHelper() + { } + +private: + virtual void + OnPromptComplete(PermissionValue aPermissionValue) override; +}; + +class PermissionRequestChildProcessActor final + : public PIndexedDBPermissionRequestChild +{ + BackgroundFactoryRequestChild* mActor; + RefPtr<IDBFactory> mFactory; + +public: + PermissionRequestChildProcessActor(BackgroundFactoryRequestChild* aActor, + IDBFactory* aFactory) + : mActor(aActor) + , mFactory(aFactory) + { + MOZ_ASSERT(aActor); + MOZ_ASSERT(aFactory); + aActor->AssertIsOnOwningThread(); + } + +protected: + ~PermissionRequestChildProcessActor() + { } + + virtual bool + Recv__delete__(const uint32_t& aPermission) override; +}; + +void +DeserializeStructuredCloneFiles( + IDBDatabase* aDatabase, + const nsTArray<SerializedStructuredCloneFile>& aSerializedFiles, + const nsTArray<RefPtr<JS::WasmModule>>* aModuleSet, + nsTArray<StructuredCloneFile>& aFiles) +{ + MOZ_ASSERT_IF(aModuleSet, !aModuleSet->IsEmpty()); + MOZ_ASSERT(aFiles.IsEmpty()); + + if (!aSerializedFiles.IsEmpty()) { + uint32_t moduleIndex = 0; + + const uint32_t count = aSerializedFiles.Length(); + aFiles.SetCapacity(count); + + for (uint32_t index = 0; index < count; index++) { + const SerializedStructuredCloneFile& serializedFile = + aSerializedFiles[index]; + + const BlobOrMutableFile& blobOrMutableFile = serializedFile.file(); + + switch (serializedFile.type()) { + case StructuredCloneFile::eBlob: { + MOZ_ASSERT(blobOrMutableFile.type() == BlobOrMutableFile::TPBlobChild); + + auto* actor = + static_cast<BlobChild*>(blobOrMutableFile.get_PBlobChild()); + + RefPtr<BlobImpl> blobImpl = actor->GetBlobImpl(); + MOZ_ASSERT(blobImpl); + + RefPtr<Blob> blob = Blob::Create(aDatabase->GetOwner(), blobImpl); + + aDatabase->NoteReceivedBlob(blob); + + StructuredCloneFile* file = aFiles.AppendElement(); + MOZ_ASSERT(file); + + file->mType = StructuredCloneFile::eBlob; + file->mBlob.swap(blob); + + break; + } + + case StructuredCloneFile::eMutableFile: { + MOZ_ASSERT(blobOrMutableFile.type() == BlobOrMutableFile::Tnull_t || + blobOrMutableFile.type() == + BlobOrMutableFile::TPBackgroundMutableFileChild); + + switch (blobOrMutableFile.type()) { + case BlobOrMutableFile::Tnull_t: { + StructuredCloneFile* file = aFiles.AppendElement(); + MOZ_ASSERT(file); + + file->mType = StructuredCloneFile::eMutableFile; + + break; + } + + case BlobOrMutableFile::TPBackgroundMutableFileChild: { + auto* actor = + static_cast<BackgroundMutableFileChild*>( + blobOrMutableFile.get_PBackgroundMutableFileChild()); + MOZ_ASSERT(actor); + + actor->EnsureDOMObject(); + + auto* mutableFile = + static_cast<IDBMutableFile*>(actor->GetDOMObject()); + MOZ_ASSERT(mutableFile); + + StructuredCloneFile* file = aFiles.AppendElement(); + MOZ_ASSERT(file); + + file->mType = StructuredCloneFile::eMutableFile; + file->mMutableFile = mutableFile; + + actor->ReleaseDOMObject(); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + break; + } + + case StructuredCloneFile::eStructuredClone: { + StructuredCloneFile* file = aFiles.AppendElement(); + MOZ_ASSERT(file); + + file->mType = StructuredCloneFile::eStructuredClone; + + break; + } + + case StructuredCloneFile::eWasmBytecode: + case StructuredCloneFile::eWasmCompiled: { + if (aModuleSet) { + MOZ_ASSERT(blobOrMutableFile.type() == BlobOrMutableFile::Tnull_t); + + StructuredCloneFile* file = aFiles.AppendElement(); + MOZ_ASSERT(file); + + file->mType = serializedFile.type(); + + MOZ_ASSERT(moduleIndex < aModuleSet->Length()); + file->mWasmModule = aModuleSet->ElementAt(moduleIndex); + + if (serializedFile.type() == StructuredCloneFile::eWasmCompiled) { + moduleIndex++; + } + + break; + } + + MOZ_ASSERT(blobOrMutableFile.type() == + BlobOrMutableFile::TPBlobChild); + + auto* actor = + static_cast<BlobChild*>(blobOrMutableFile.get_PBlobChild()); + + RefPtr<BlobImpl> blobImpl = actor->GetBlobImpl(); + MOZ_ASSERT(blobImpl); + + RefPtr<Blob> blob = Blob::Create(aDatabase->GetOwner(), blobImpl); + + aDatabase->NoteReceivedBlob(blob); + + StructuredCloneFile* file = aFiles.AppendElement(); + MOZ_ASSERT(file); + + file->mType = serializedFile.type(); + file->mBlob.swap(blob); + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + } +} + +void +DispatchErrorEvent(IDBRequest* aRequest, + nsresult aErrorCode, + IDBTransaction* aTransaction = nullptr, + nsIDOMEvent* aEvent = nullptr) +{ + MOZ_ASSERT(aRequest); + aRequest->AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_DOM_INDEXEDDB); + + PROFILER_LABEL("IndexedDB", + "DispatchErrorEvent", + js::ProfileEntry::Category::STORAGE); + + RefPtr<IDBRequest> request = aRequest; + RefPtr<IDBTransaction> transaction = aTransaction; + + request->SetError(aErrorCode); + + nsCOMPtr<nsIDOMEvent> errorEvent; + if (!aEvent) { + // Make an error event and fire it at the target. + errorEvent = CreateGenericEvent(request, + nsDependentString(kErrorEventType), + eDoesBubble, + eCancelable); + MOZ_ASSERT(errorEvent); + + aEvent = errorEvent; + } + + Maybe<AutoSetCurrentTransaction> asct; + if (aTransaction) { + asct.emplace(aTransaction); + } + + if (transaction) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "Firing %s event with error 0x%x", + "IndexedDB %s: C T[%lld] R[%llu]: %s (0x%x)", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kErrorEventType), + aErrorCode); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Request[%llu]: " + "Firing %s event with error 0x%x", + "IndexedDB %s: C R[%llu]: %s (0x%x)", + IDB_LOG_ID_STRING(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kErrorEventType), + aErrorCode); + } + + bool doDefault; + nsresult rv = request->DispatchEvent(aEvent, &doDefault); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT(!transaction || transaction->IsOpen() || transaction->IsAborted()); + + // Do not abort the transaction here if this request is failed due to the + // abortion of its transaction to ensure that the correct error cause of + // the abort event be set in IDBTransaction::FireCompleteOrAbortEvents() later. + if (transaction && transaction->IsOpen() && + aErrorCode != NS_ERROR_DOM_INDEXEDDB_ABORT_ERR) { + WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); + MOZ_ASSERT(internalEvent); + + if (internalEvent->mFlags.mExceptionWasRaised) { + transaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } else if (doDefault) { + transaction->Abort(request); + } + } +} + +void +DispatchSuccessEvent(ResultHelper* aResultHelper, + nsIDOMEvent* aEvent = nullptr) +{ + MOZ_ASSERT(aResultHelper); + + PROFILER_LABEL("IndexedDB", + "DispatchSuccessEvent", + js::ProfileEntry::Category::STORAGE); + + RefPtr<IDBRequest> request = aResultHelper->Request(); + MOZ_ASSERT(request); + request->AssertIsOnOwningThread(); + + RefPtr<IDBTransaction> transaction = aResultHelper->Transaction(); + + if (transaction && transaction->IsAborted()) { + DispatchErrorEvent(request, transaction->AbortCode(), transaction); + return; + } + + nsCOMPtr<nsIDOMEvent> successEvent; + if (!aEvent) { + successEvent = CreateGenericEvent(request, + nsDependentString(kSuccessEventType), + eDoesNotBubble, + eNotCancelable); + MOZ_ASSERT(successEvent); + + aEvent = successEvent; + } + + request->SetResultCallback(aResultHelper); + + MOZ_ASSERT(aEvent); + MOZ_ASSERT_IF(transaction, transaction->IsOpen()); + + if (transaction) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "Firing %s event", + "IndexedDB %s: C T[%lld] R[%llu]: %s", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kSuccessEventType)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Request[%llu]: Firing %s event", + "IndexedDB %s: C R[%llu]: %s", + IDB_LOG_ID_STRING(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kSuccessEventType)); + } + + bool dummy; + nsresult rv = request->DispatchEvent(aEvent, &dummy); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT_IF(transaction, + transaction->IsOpen() || transaction->IsAborted()); + + WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); + MOZ_ASSERT(internalEvent); + + if (transaction && + transaction->IsOpen() && + internalEvent->mFlags.mExceptionWasRaised) { + transaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } +} + +PRFileDesc* +GetFileDescriptorFromStream(nsIInputStream* aStream) +{ + MOZ_ASSERT(aStream); + + nsCOMPtr<nsIFileMetadata> fileMetadata = do_QueryInterface(aStream); + if (NS_WARN_IF(!fileMetadata)) { + return nullptr; + } + + PRFileDesc* fileDesc; + nsresult rv = fileMetadata->GetFileDescriptor(&fileDesc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + MOZ_ASSERT(fileDesc); + + return fileDesc; +} + +class WorkerPermissionChallenge; + +// This class calles WorkerPermissionChallenge::OperationCompleted() in the +// worker thread. +class WorkerPermissionOperationCompleted final : public WorkerControlRunnable +{ + RefPtr<WorkerPermissionChallenge> mChallenge; + +public: + WorkerPermissionOperationCompleted(WorkerPrivate* aWorkerPrivate, + WorkerPermissionChallenge* aChallenge) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + , mChallenge(aChallenge) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; +}; + +// This class used to do prompting in the main thread and main process. +class WorkerPermissionRequest final : public PermissionRequestBase +{ + RefPtr<WorkerPermissionChallenge> mChallenge; + +public: + WorkerPermissionRequest(Element* aElement, + nsIPrincipal* aPrincipal, + WorkerPermissionChallenge* aChallenge) + : PermissionRequestBase(aElement, aPrincipal) + , mChallenge(aChallenge) + { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aChallenge); + } + +private: + ~WorkerPermissionRequest() + { + MOZ_ASSERT(NS_IsMainThread()); + } + + virtual void + OnPromptComplete(PermissionValue aPermissionValue) override; +}; + +// This class is used in the main thread of all child processes. +class WorkerPermissionRequestChildProcessActor final + : public PIndexedDBPermissionRequestChild +{ + RefPtr<WorkerPermissionChallenge> mChallenge; + +public: + explicit WorkerPermissionRequestChildProcessActor( + WorkerPermissionChallenge* aChallenge) + : mChallenge(aChallenge) + { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aChallenge); + } + +protected: + ~WorkerPermissionRequestChildProcessActor() + {} + + virtual bool + Recv__delete__(const uint32_t& aPermission) override; +}; + +class WorkerPermissionChallenge final : public Runnable +{ +public: + WorkerPermissionChallenge(WorkerPrivate* aWorkerPrivate, + BackgroundFactoryRequestChild* aActor, + IDBFactory* aFactory, + const PrincipalInfo& aPrincipalInfo) + : mWorkerPrivate(aWorkerPrivate) + , mActor(aActor) + , mFactory(aFactory) + , mPrincipalInfo(aPrincipalInfo) + { + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aFactory); + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool + Dispatch() + { + mWorkerPrivate->AssertIsOnWorkerThread(); + if (NS_WARN_IF(!mWorkerPrivate->ModifyBusyCountFromWorker(true))) { + return false; + } + + if (NS_WARN_IF(NS_FAILED(NS_DispatchToMainThread(this)))) { + mWorkerPrivate->ModifyBusyCountFromWorker(false); + return false; + } + + return true; + } + + NS_IMETHOD + Run() override + { + bool completed = RunInternal(); + if (completed) { + OperationCompleted(); + } + + return NS_OK; + } + + void + OperationCompleted() + { + if (NS_IsMainThread()) { + RefPtr<WorkerPermissionOperationCompleted> runnable = + new WorkerPermissionOperationCompleted(mWorkerPrivate, this); + + MOZ_ALWAYS_TRUE(runnable->Dispatch()); + return; + } + + MOZ_ASSERT(mActor); + mActor->AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + RefPtr<IDBFactory> factory; + mFactory.swap(factory); + + mActor->SendPermissionRetry(); + mActor = nullptr; + + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->ModifyBusyCountFromWorker(false); + } + +private: + bool + RunInternal() + { + MOZ_ASSERT(NS_IsMainThread()); + + // Walk up to our containing page + WorkerPrivate* wp = mWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + nsPIDOMWindowInner* window = wp->GetWindow(); + if (!window) { + return true; + } + + nsresult rv; + nsCOMPtr<nsIPrincipal> principal = + mozilla::ipc::PrincipalInfoToPrincipal(mPrincipalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return true; + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<Element> ownerElement = + do_QueryInterface(window->GetChromeEventHandler()); + if (NS_WARN_IF(!ownerElement)) { + return true; + } + + RefPtr<WorkerPermissionRequest> helper = + new WorkerPermissionRequest(ownerElement, principal, this); + + PermissionRequestBase::PermissionValue permission; + if (NS_WARN_IF(NS_FAILED(helper->PromptIfNeeded(&permission)))) { + return true; + } + + MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed || + permission == PermissionRequestBase::kPermissionDenied || + permission == PermissionRequestBase::kPermissionPrompt); + + return permission != PermissionRequestBase::kPermissionPrompt; + } + + TabChild* tabChild = TabChild::GetFrom(window); + MOZ_ASSERT(tabChild); + + IPC::Principal ipcPrincipal(principal); + + auto* actor = new WorkerPermissionRequestChildProcessActor(this); + tabChild->SendPIndexedDBPermissionRequestConstructor(actor, ipcPrincipal); + return false; + } + +private: + WorkerPrivate* mWorkerPrivate; + BackgroundFactoryRequestChild* mActor; + RefPtr<IDBFactory> mFactory; + PrincipalInfo mPrincipalInfo; +}; + +void +WorkerPermissionRequest::OnPromptComplete(PermissionValue aPermissionValue) +{ + MOZ_ASSERT(NS_IsMainThread()); + mChallenge->OperationCompleted(); +} + +bool +WorkerPermissionOperationCompleted::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + mChallenge->OperationCompleted(); + return true; +} + +bool +WorkerPermissionRequestChildProcessActor::Recv__delete__( + const uint32_t& /* aPermission */) +{ + MOZ_ASSERT(NS_IsMainThread()); + mChallenge->OperationCompleted(); + return true; +} + +} // namespace + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +// CancelableRunnable is used to make workers happy. +class BackgroundRequestChild::PreprocessHelper final + : public CancelableRunnable +{ + typedef std::pair<nsCOMPtr<nsIInputStream>, + nsCOMPtr<nsIInputStream>> StreamPair; + + nsCOMPtr<nsIEventTarget> mOwningThread; + nsTArray<StreamPair> mStreamPairs; + nsTArray<RefPtr<JS::WasmModule>> mModuleSet; + BackgroundRequestChild* mActor; + uint32_t mModuleSetIndex; + nsresult mResultCode; + +public: + PreprocessHelper(uint32_t aModuleSetIndex, BackgroundRequestChild* aActor) + : mOwningThread(NS_GetCurrentThread()) + , mActor(aActor) + , mModuleSetIndex(aModuleSetIndex) + , mResultCode(NS_OK) + { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + aActor->AssertIsOnOwningThread(); + } + + bool + IsOnOwningThread() const + { + MOZ_ASSERT(mOwningThread); + + bool current; + return NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t)) && current; + } + + void + AssertIsOnOwningThread() const + { + MOZ_ASSERT(IsOnOwningThread()); + } + + void + ClearActor() + { + AssertIsOnOwningThread(); + + mActor = nullptr; + } + + nsresult + Init(const nsTArray<StructuredCloneFile>& aFiles); + + nsresult + Dispatch(); + +private: + ~PreprocessHelper() + { } + + void + RunOnOwningThread(); + + nsresult + RunOnStreamTransportThread(); + + NS_DECL_NSIRUNNABLE + + virtual nsresult + Cancel() override; +}; + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +void +PermissionRequestMainProcessHelper::OnPromptComplete( + PermissionValue aPermissionValue) +{ + MOZ_ASSERT(mActor); + mActor->AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + mActor->SendPermissionRetry(); + + mActor = nullptr; + mFactory = nullptr; +} + +bool +PermissionRequestChildProcessActor::Recv__delete__( + const uint32_t& /* aPermission */) +{ + MOZ_ASSERT(mActor); + mActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mFactory); + + MaybeCollectGarbageOnIPCMessage(); + + RefPtr<IDBFactory> factory; + mFactory.swap(factory); + + mActor->SendPermissionRetry(); + mActor = nullptr; + + return true; +} + +/******************************************************************************* + * BackgroundRequestChildBase + ******************************************************************************/ + +BackgroundRequestChildBase::BackgroundRequestChildBase(IDBRequest* aRequest) + : mRequest(aRequest) +{ + MOZ_ASSERT(aRequest); + aRequest->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundRequestChildBase); +} + +BackgroundRequestChildBase::~BackgroundRequestChildBase() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(indexedDB::BackgroundRequestChildBase); +} + +#ifdef DEBUG + +void +BackgroundRequestChildBase::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mRequest); + mRequest->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +/******************************************************************************* + * BackgroundFactoryChild + ******************************************************************************/ + +BackgroundFactoryChild::BackgroundFactoryChild(IDBFactory* aFactory) + : mFactory(aFactory) +#ifdef DEBUG + , mOwningThread(NS_GetCurrentThread()) +#endif +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundFactoryChild); +} + +BackgroundFactoryChild::~BackgroundFactoryChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundFactoryChild); +} + +#ifdef DEBUG + +void +BackgroundFactoryChild::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mOwningThread); + + bool current; + MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t))); + MOZ_ASSERT(current); +} + +nsIEventTarget* +BackgroundFactoryChild::OwningThread() const +{ + MOZ_ASSERT(mOwningThread); + return mOwningThread; +} + +#endif // DEBUG + +void +BackgroundFactoryChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + + if (mFactory) { + mFactory->ClearBackgroundActor(); + mFactory = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundIDBFactoryChild::SendDeleteMe()); + } +} + +void +BackgroundFactoryChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mFactory) { + mFactory->ClearBackgroundActor(); +#ifdef DEBUG + mFactory = nullptr; +#endif + } +} + +PBackgroundIDBFactoryRequestChild* +BackgroundFactoryChild::AllocPBackgroundIDBFactoryRequestChild( + const FactoryRequestParams& aParams) +{ + MOZ_CRASH("PBackgroundIDBFactoryRequestChild actors should be manually " + "constructed!"); +} + +bool +BackgroundFactoryChild::DeallocPBackgroundIDBFactoryRequestChild( + PBackgroundIDBFactoryRequestChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundFactoryRequestChild*>(aActor); + return true; +} + +PBackgroundIDBDatabaseChild* +BackgroundFactoryChild::AllocPBackgroundIDBDatabaseChild( + const DatabaseSpec& aSpec, + PBackgroundIDBFactoryRequestChild* aRequest) +{ + AssertIsOnOwningThread(); + + auto request = static_cast<BackgroundFactoryRequestChild*>(aRequest); + MOZ_ASSERT(request); + + return new BackgroundDatabaseChild(aSpec, request); +} + +bool +BackgroundFactoryChild::DeallocPBackgroundIDBDatabaseChild( + PBackgroundIDBDatabaseChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundDatabaseChild*>(aActor); + return true; +} + +/******************************************************************************* + * BackgroundFactoryRequestChild + ******************************************************************************/ + +BackgroundFactoryRequestChild::BackgroundFactoryRequestChild( + IDBFactory* aFactory, + IDBOpenDBRequest* aOpenRequest, + bool aIsDeleteOp, + uint64_t aRequestedVersion) + : BackgroundRequestChildBase(aOpenRequest) + , mFactory(aFactory) + , mRequestedVersion(aRequestedVersion) + , mIsDeleteOp(aIsDeleteOp) +{ + // Can't assert owning thread here because IPDL has not yet set our manager! + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aOpenRequest); + + MOZ_COUNT_CTOR(indexedDB::BackgroundFactoryRequestChild); +} + +BackgroundFactoryRequestChild::~BackgroundFactoryRequestChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundFactoryRequestChild); +} + +IDBOpenDBRequest* +BackgroundFactoryRequestChild::GetOpenDBRequest() const +{ + AssertIsOnOwningThread(); + + return static_cast<IDBOpenDBRequest*>(mRequest.get()); +} + +bool +BackgroundFactoryRequestChild::HandleResponse(nsresult aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResponse)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aResponse) == NS_ERROR_MODULE_DOM_INDEXEDDB); + + mRequest->Reset(); + + DispatchErrorEvent(mRequest, aResponse); + + return true; +} + +bool +BackgroundFactoryRequestChild::HandleResponse( + const OpenDatabaseRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + + mRequest->Reset(); + + auto databaseActor = + static_cast<BackgroundDatabaseChild*>(aResponse.databaseChild()); + MOZ_ASSERT(databaseActor); + + IDBDatabase* database = databaseActor->GetDOMObject(); + if (!database) { + databaseActor->EnsureDOMObject(); + + database = databaseActor->GetDOMObject(); + MOZ_ASSERT(database); + + MOZ_ASSERT(!database->IsClosed()); + } + + if (database->IsClosed()) { + // If the database was closed already, which is only possible if we fired an + // "upgradeneeded" event, then we shouldn't fire a "success" event here. + // Instead we fire an error event with AbortErr. + DispatchErrorEvent(mRequest, NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } else { + ResultHelper helper(mRequest, nullptr, database); + + DispatchSuccessEvent(&helper); + } + + databaseActor->ReleaseDOMObject(); + + return true; +} + +bool +BackgroundFactoryRequestChild::HandleResponse( + const DeleteDatabaseRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + + ResultHelper helper(mRequest, nullptr, &JS::UndefinedHandleValue); + + nsCOMPtr<nsIDOMEvent> successEvent = + IDBVersionChangeEvent::Create(mRequest, + nsDependentString(kSuccessEventType), + aResponse.previousVersion()); + MOZ_ASSERT(successEvent); + + DispatchSuccessEvent(&helper, successEvent); + + return true; +} + +void +BackgroundFactoryRequestChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (aWhy != Deletion) { + IDBOpenDBRequest* openRequest = GetOpenDBRequest(); + if (openRequest) { + openRequest->NoteComplete(); + } + } +} + +bool +BackgroundFactoryRequestChild::Recv__delete__( + const FactoryRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + MaybeCollectGarbageOnIPCMessage(); + + bool result; + + switch (aResponse.type()) { + case FactoryRequestResponse::Tnsresult: + result = HandleResponse(aResponse.get_nsresult()); + break; + + case FactoryRequestResponse::TOpenDatabaseRequestResponse: + result = HandleResponse(aResponse.get_OpenDatabaseRequestResponse()); + break; + + case FactoryRequestResponse::TDeleteDatabaseRequestResponse: + result = HandleResponse(aResponse.get_DeleteDatabaseRequestResponse()); + break; + + default: + MOZ_CRASH("Unknown response type!"); + } + + IDBOpenDBRequest* request = GetOpenDBRequest(); + MOZ_ASSERT(request); + + request->NoteComplete(); + + if (NS_WARN_IF(!result)) { + return false; + } + + return true; +} + +bool +BackgroundFactoryRequestChild::RecvPermissionChallenge( + const PrincipalInfo& aPrincipalInfo) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + RefPtr<WorkerPermissionChallenge> challenge = + new WorkerPermissionChallenge(workerPrivate, this, mFactory, + aPrincipalInfo); + return challenge->Dispatch(); + } + + nsresult rv; + nsCOMPtr<nsIPrincipal> principal = + mozilla::ipc::PrincipalInfoToPrincipal(aPrincipalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsPIDOMWindowInner> window = mFactory->GetParentObject(); + MOZ_ASSERT(window); + + nsCOMPtr<Element> ownerElement = + do_QueryInterface(window->GetChromeEventHandler()); + if (NS_WARN_IF(!ownerElement)) { + // If this fails, the page was navigated. Fail the permission check by + // forcing an immediate retry. + return SendPermissionRetry(); + } + + RefPtr<PermissionRequestMainProcessHelper> helper = + new PermissionRequestMainProcessHelper(this, mFactory, ownerElement, principal); + + PermissionRequestBase::PermissionValue permission; + if (NS_WARN_IF(NS_FAILED(helper->PromptIfNeeded(&permission)))) { + return false; + } + + MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed || + permission == PermissionRequestBase::kPermissionDenied || + permission == PermissionRequestBase::kPermissionPrompt); + + if (permission != PermissionRequestBase::kPermissionPrompt) { + SendPermissionRetry(); + } + return true; + } + + RefPtr<TabChild> tabChild = mFactory->GetTabChild(); + MOZ_ASSERT(tabChild); + + IPC::Principal ipcPrincipal(principal); + + auto* actor = new PermissionRequestChildProcessActor(this, mFactory); + + tabChild->SendPIndexedDBPermissionRequestConstructor(actor, ipcPrincipal); + + return true; +} + +bool +BackgroundFactoryRequestChild::RecvBlocked(const uint64_t& aCurrentVersion) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + MaybeCollectGarbageOnIPCMessage(); + + const nsDependentString type(kBlockedEventType); + + nsCOMPtr<nsIDOMEvent> blockedEvent; + if (mIsDeleteOp) { + blockedEvent = + IDBVersionChangeEvent::Create(mRequest, type, aCurrentVersion); + MOZ_ASSERT(blockedEvent); + } else { + blockedEvent = + IDBVersionChangeEvent::Create(mRequest, + type, + aCurrentVersion, + mRequestedVersion); + MOZ_ASSERT(blockedEvent); + } + + RefPtr<IDBRequest> kungFuDeathGrip = mRequest; + + IDB_LOG_MARK("IndexedDB %s: Child Request[%llu]: Firing \"blocked\" event", + "IndexedDB %s: C R[%llu]: \"blocked\"", + IDB_LOG_ID_STRING(), + kungFuDeathGrip->LoggingSerialNumber()); + + bool dummy; + if (NS_FAILED(kungFuDeathGrip->DispatchEvent(blockedEvent, &dummy))) { + NS_WARNING("Failed to dispatch event!"); + } + + return true; +} + +/******************************************************************************* + * BackgroundDatabaseChild + ******************************************************************************/ + +BackgroundDatabaseChild::BackgroundDatabaseChild( + const DatabaseSpec& aSpec, + BackgroundFactoryRequestChild* aOpenRequestActor) + : mSpec(new DatabaseSpec(aSpec)) + , mOpenRequestActor(aOpenRequestActor) + , mDatabase(nullptr) +{ + // Can't assert owning thread here because IPDL has not yet set our manager! + MOZ_ASSERT(aOpenRequestActor); + + MOZ_COUNT_CTOR(indexedDB::BackgroundDatabaseChild); +} + +BackgroundDatabaseChild::~BackgroundDatabaseChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundDatabaseChild); +} + +void +BackgroundDatabaseChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mTemporaryStrongDatabase); + MOZ_ASSERT(!mOpenRequestActor); + + if (mDatabase) { + mDatabase->ClearBackgroundActor(); + mDatabase = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundIDBDatabaseChild::SendDeleteMe()); + } +} + +void +BackgroundDatabaseChild::EnsureDOMObject() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenRequestActor); + + if (mTemporaryStrongDatabase) { + MOZ_ASSERT(!mSpec); + return; + } + + MOZ_ASSERT(mSpec); + + auto request = mOpenRequestActor->GetOpenDBRequest(); + MOZ_ASSERT(request); + + auto factory = + static_cast<BackgroundFactoryChild*>(Manager())->GetDOMObject(); + MOZ_ASSERT(factory); + + mTemporaryStrongDatabase = + IDBDatabase::Create(request, factory, this, mSpec); + + MOZ_ASSERT(mTemporaryStrongDatabase); + mTemporaryStrongDatabase->AssertIsOnOwningThread(); + + mDatabase = mTemporaryStrongDatabase; + mSpec.forget(); +} + +void +BackgroundDatabaseChild::ReleaseDOMObject() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTemporaryStrongDatabase); + mTemporaryStrongDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenRequestActor); + MOZ_ASSERT(mDatabase == mTemporaryStrongDatabase); + + mOpenRequestActor = nullptr; + + // This may be the final reference to the IDBDatabase object so we may end up + // calling SendDeleteMeInternal() here. Make sure everything is cleaned up + // properly before proceeding. + mTemporaryStrongDatabase = nullptr; +} + +void +BackgroundDatabaseChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mDatabase) { + mDatabase->ClearBackgroundActor(); +#ifdef DEBUG + mDatabase = nullptr; +#endif + } +} + +PBackgroundIDBDatabaseFileChild* +BackgroundDatabaseChild::AllocPBackgroundIDBDatabaseFileChild( + PBlobChild* aBlobChild) +{ + MOZ_CRASH("PBackgroundIDBFileChild actors should be manually constructed!"); +} + +bool +BackgroundDatabaseChild::DeallocPBackgroundIDBDatabaseFileChild( + PBackgroundIDBDatabaseFileChild* aActor) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +PBackgroundIDBDatabaseRequestChild* +BackgroundDatabaseChild::AllocPBackgroundIDBDatabaseRequestChild( + const DatabaseRequestParams& aParams) +{ + MOZ_CRASH("PBackgroundIDBDatabaseRequestChild actors should be manually " + "constructed!"); +} + +bool +BackgroundDatabaseChild::DeallocPBackgroundIDBDatabaseRequestChild( + PBackgroundIDBDatabaseRequestChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundDatabaseRequestChild*>(aActor); + return true; +} + +PBackgroundIDBTransactionChild* +BackgroundDatabaseChild::AllocPBackgroundIDBTransactionChild( + const nsTArray<nsString>& aObjectStoreNames, + const Mode& aMode) +{ + MOZ_CRASH("PBackgroundIDBTransactionChild actors should be manually " + "constructed!"); +} + +bool +BackgroundDatabaseChild::DeallocPBackgroundIDBTransactionChild( + PBackgroundIDBTransactionChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundTransactionChild*>(aActor); + return true; +} + +PBackgroundIDBVersionChangeTransactionChild* +BackgroundDatabaseChild::AllocPBackgroundIDBVersionChangeTransactionChild( + const uint64_t& aCurrentVersion, + const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, + const int64_t& aNextIndexId) +{ + AssertIsOnOwningThread(); + + IDBOpenDBRequest* request = mOpenRequestActor->GetOpenDBRequest(); + MOZ_ASSERT(request); + + return new BackgroundVersionChangeTransactionChild(request); +} + +bool +BackgroundDatabaseChild::RecvPBackgroundIDBVersionChangeTransactionConstructor( + PBackgroundIDBVersionChangeTransactionChild* aActor, + const uint64_t& aCurrentVersion, + const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, + const int64_t& aNextIndexId) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(mOpenRequestActor); + + MaybeCollectGarbageOnIPCMessage(); + + EnsureDOMObject(); + + auto* actor = static_cast<BackgroundVersionChangeTransactionChild*>(aActor); + + RefPtr<IDBOpenDBRequest> request = mOpenRequestActor->GetOpenDBRequest(); + MOZ_ASSERT(request); + + RefPtr<IDBTransaction> transaction = + IDBTransaction::CreateVersionChange(mDatabase, + actor, + request, + aNextObjectStoreId, + aNextIndexId); + if (NS_WARN_IF(!transaction)) { + // This can happen if we receive events after a worker has begun its + // shutdown process. + MOZ_ASSERT(!NS_IsMainThread()); + + // Report this to the console. + IDB_REPORT_INTERNAL_ERR(); + + MOZ_ALWAYS_TRUE(aActor->SendDeleteMe()); + return true; + } + + transaction->AssertIsOnOwningThread(); + + actor->SetDOMTransaction(transaction); + + mDatabase->EnterSetVersionTransaction(aRequestedVersion); + + request->SetTransaction(transaction); + + nsCOMPtr<nsIDOMEvent> upgradeNeededEvent = + IDBVersionChangeEvent::Create(request, + nsDependentString(kUpgradeNeededEventType), + aCurrentVersion, + aRequestedVersion); + MOZ_ASSERT(upgradeNeededEvent); + + ResultHelper helper(request, transaction, mDatabase); + + DispatchSuccessEvent(&helper, upgradeNeededEvent); + + return true; +} + +bool +BackgroundDatabaseChild::DeallocPBackgroundIDBVersionChangeTransactionChild( + PBackgroundIDBVersionChangeTransactionChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundVersionChangeTransactionChild*>(aActor); + return true; +} + +PBackgroundMutableFileChild* +BackgroundDatabaseChild::AllocPBackgroundMutableFileChild(const nsString& aName, + const nsString& aType) +{ + AssertIsOnOwningThread(); + +#ifdef DEBUG + nsCOMPtr<nsIThread> owningThread = do_QueryInterface(OwningThread()); + + PRThread* owningPRThread; + owningThread->GetPRThread(&owningPRThread); +#endif + + return new BackgroundMutableFileChild(DEBUGONLY(owningPRThread,) + aName, + aType); +} + +bool +BackgroundDatabaseChild::DeallocPBackgroundMutableFileChild( + PBackgroundMutableFileChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundMutableFileChild*>(aActor); + return true; +} + +bool +BackgroundDatabaseChild::RecvVersionChange(const uint64_t& aOldVersion, + const NullableVersion& aNewVersion) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (!mDatabase || mDatabase->IsClosed()) { + return true; + } + + RefPtr<IDBDatabase> kungFuDeathGrip = mDatabase; + + // Handle bfcache'd windows. + if (nsPIDOMWindowInner* owner = kungFuDeathGrip->GetOwner()) { + // The database must be closed if the window is already frozen. + bool shouldAbortAndClose = owner->IsFrozen(); + + // Anything in the bfcache has to be evicted and then we have to close the + // database also. + if (nsCOMPtr<nsIDocument> doc = owner->GetExtantDoc()) { + if (nsCOMPtr<nsIBFCacheEntry> bfCacheEntry = doc->GetBFCacheEntry()) { + bfCacheEntry->RemoveFromBFCacheSync(); + shouldAbortAndClose = true; + } + } + + if (shouldAbortAndClose) { + // Invalidate() doesn't close the database in the parent, so we have + // to call Close() and AbortTransactions() manually. + kungFuDeathGrip->AbortTransactions(/* aShouldWarn */ false); + kungFuDeathGrip->Close(); + return true; + } + } + + // Otherwise fire a versionchange event. + const nsDependentString type(kVersionChangeEventType); + + nsCOMPtr<nsIDOMEvent> versionChangeEvent; + + switch (aNewVersion.type()) { + case NullableVersion::Tnull_t: + versionChangeEvent = + IDBVersionChangeEvent::Create(kungFuDeathGrip, type, aOldVersion); + MOZ_ASSERT(versionChangeEvent); + break; + + case NullableVersion::Tuint64_t: + versionChangeEvent = + IDBVersionChangeEvent::Create(kungFuDeathGrip, + type, + aOldVersion, + aNewVersion.get_uint64_t()); + MOZ_ASSERT(versionChangeEvent); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + IDB_LOG_MARK("IndexedDB %s: Child : Firing \"versionchange\" event", + "IndexedDB %s: C: IDBDatabase \"versionchange\" event", + IDB_LOG_ID_STRING()); + + bool dummy; + if (NS_FAILED(kungFuDeathGrip->DispatchEvent(versionChangeEvent, &dummy))) { + NS_WARNING("Failed to dispatch event!"); + } + + if (!kungFuDeathGrip->IsClosed()) { + SendBlocked(); + } + + return true; +} + +bool +BackgroundDatabaseChild::RecvInvalidate() +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mDatabase) { + mDatabase->Invalidate(); + } + + return true; +} + +bool +BackgroundDatabaseChild::RecvCloseAfterInvalidationComplete() +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mDatabase) { + mDatabase->DispatchTrustedEvent(nsDependentString(kCloseEventType)); + } + + return true; +} + +/******************************************************************************* + * BackgroundDatabaseRequestChild + ******************************************************************************/ + +BackgroundDatabaseRequestChild::BackgroundDatabaseRequestChild( + IDBDatabase* aDatabase, + IDBRequest* aRequest) + : BackgroundRequestChildBase(aRequest) + , mDatabase(aDatabase) +{ + // Can't assert owning thread here because IPDL has not yet set our manager! + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(aRequest); + + MOZ_COUNT_CTOR(indexedDB::BackgroundDatabaseRequestChild); +} + +BackgroundDatabaseRequestChild::~BackgroundDatabaseRequestChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundDatabaseRequestChild); +} + +bool +BackgroundDatabaseRequestChild::HandleResponse(nsresult aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResponse)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aResponse) == NS_ERROR_MODULE_DOM_INDEXEDDB); + + mRequest->Reset(); + + DispatchErrorEvent(mRequest, aResponse); + + return true; +} + +bool +BackgroundDatabaseRequestChild::HandleResponse( + const CreateFileRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + + mRequest->Reset(); + + auto mutableFileActor = + static_cast<BackgroundMutableFileChild*>(aResponse.mutableFileChild()); + MOZ_ASSERT(mutableFileActor); + + mutableFileActor->EnsureDOMObject(); + + auto mutableFile = + static_cast<IDBMutableFile*>(mutableFileActor->GetDOMObject()); + MOZ_ASSERT(mutableFile); + + ResultHelper helper(mRequest, nullptr, mutableFile); + + DispatchSuccessEvent(&helper); + + mutableFileActor->ReleaseDOMObject(); + + return true; +} + +bool +BackgroundDatabaseRequestChild::Recv__delete__( + const DatabaseRequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + switch (aResponse.type()) { + case DatabaseRequestResponse::Tnsresult: + return HandleResponse(aResponse.get_nsresult()); + + case DatabaseRequestResponse::TCreateFileRequestResponse: + return HandleResponse(aResponse.get_CreateFileRequestResponse()); + + default: + MOZ_CRASH("Unknown response type!"); + } + + MOZ_CRASH("Should never get here!"); +} + +/******************************************************************************* + * BackgroundTransactionBase + ******************************************************************************/ + +BackgroundTransactionBase::BackgroundTransactionBase() +: mTransaction(nullptr) +{ + MOZ_COUNT_CTOR(indexedDB::BackgroundTransactionBase); +} + +BackgroundTransactionBase::BackgroundTransactionBase( + IDBTransaction* aTransaction) + : mTemporaryStrongTransaction(aTransaction) + , mTransaction(aTransaction) +{ + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundTransactionBase); +} + +BackgroundTransactionBase::~BackgroundTransactionBase() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundTransactionBase); +} + +#ifdef DEBUG + +void +BackgroundTransactionBase::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void +BackgroundTransactionBase::NoteActorDestroyed() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mTemporaryStrongTransaction, mTransaction); + + if (mTransaction) { + mTransaction->ClearBackgroundActor(); + + // Normally this would be DEBUG-only but NoteActorDestroyed is also called + // from SendDeleteMeInternal. In that case we're going to receive an actual + // ActorDestroy call later and we don't want to touch a dead object. + mTemporaryStrongTransaction = nullptr; + mTransaction = nullptr; + } +} + +void +BackgroundTransactionBase::SetDOMTransaction(IDBTransaction* aTransaction) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + MOZ_ASSERT(!mTemporaryStrongTransaction); + MOZ_ASSERT(!mTransaction); + + mTemporaryStrongTransaction = aTransaction; + mTransaction = aTransaction; +} + +void +BackgroundTransactionBase::NoteComplete() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mTransaction, mTemporaryStrongTransaction); + + mTemporaryStrongTransaction = nullptr; +} + +/******************************************************************************* + * BackgroundTransactionChild + ******************************************************************************/ + +BackgroundTransactionChild::BackgroundTransactionChild( + IDBTransaction* aTransaction) + : BackgroundTransactionBase(aTransaction) +{ + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundTransactionChild); +} + +BackgroundTransactionChild::~BackgroundTransactionChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundTransactionChild); +} + +#ifdef DEBUG + +void +BackgroundTransactionChild::AssertIsOnOwningThread() const +{ + static_cast<BackgroundDatabaseChild*>(Manager())->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void +BackgroundTransactionChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + + if (mTransaction) { + NoteActorDestroyed(); + + MOZ_ALWAYS_TRUE(PBackgroundIDBTransactionChild::SendDeleteMe()); + } +} + +void +BackgroundTransactionChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + NoteActorDestroyed(); +} + +bool +BackgroundTransactionChild::RecvComplete(const nsresult& aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + MaybeCollectGarbageOnIPCMessage(); + + mTransaction->FireCompleteOrAbortEvents(aResult); + + NoteComplete(); + return true; +} + +PBackgroundIDBRequestChild* +BackgroundTransactionChild::AllocPBackgroundIDBRequestChild( + const RequestParams& aParams) +{ + MOZ_CRASH("PBackgroundIDBRequestChild actors should be manually " + "constructed!"); +} + +bool +BackgroundTransactionChild::DeallocPBackgroundIDBRequestChild( + PBackgroundIDBRequestChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundRequestChild*>(aActor); + return true; +} + +PBackgroundIDBCursorChild* +BackgroundTransactionChild::AllocPBackgroundIDBCursorChild( + const OpenCursorParams& aParams) +{ + AssertIsOnOwningThread(); + + MOZ_CRASH("PBackgroundIDBCursorChild actors should be manually constructed!"); +} + +bool +BackgroundTransactionChild::DeallocPBackgroundIDBCursorChild( + PBackgroundIDBCursorChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundCursorChild*>(aActor); + return true; +} + +/******************************************************************************* + * BackgroundVersionChangeTransactionChild + ******************************************************************************/ + +BackgroundVersionChangeTransactionChild:: +BackgroundVersionChangeTransactionChild(IDBOpenDBRequest* aOpenDBRequest) + : mOpenDBRequest(aOpenDBRequest) +{ + MOZ_ASSERT(aOpenDBRequest); + aOpenDBRequest->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundVersionChangeTransactionChild); +} + +BackgroundVersionChangeTransactionChild:: +~BackgroundVersionChangeTransactionChild() +{ + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(indexedDB::BackgroundVersionChangeTransactionChild); +} + +#ifdef DEBUG + +void +BackgroundVersionChangeTransactionChild::AssertIsOnOwningThread() const +{ + static_cast<BackgroundDatabaseChild*>(Manager())->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void +BackgroundVersionChangeTransactionChild::SendDeleteMeInternal( + bool aFailedConstructor) +{ + AssertIsOnOwningThread(); + + if (mTransaction || aFailedConstructor) { + NoteActorDestroyed(); + + MOZ_ALWAYS_TRUE(PBackgroundIDBVersionChangeTransactionChild:: + SendDeleteMe()); + } +} + +void +BackgroundVersionChangeTransactionChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + mOpenDBRequest = nullptr; + + NoteActorDestroyed(); +} + +bool +BackgroundVersionChangeTransactionChild::RecvComplete(const nsresult& aResult) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (!mTransaction) { + return true; + } + + MOZ_ASSERT(mOpenDBRequest); + + IDBDatabase* database = mTransaction->Database(); + MOZ_ASSERT(database); + + database->ExitSetVersionTransaction(); + + if (NS_FAILED(aResult)) { + database->Close(); + } + + mTransaction->FireCompleteOrAbortEvents(aResult); + + mOpenDBRequest->SetTransaction(nullptr); + mOpenDBRequest = nullptr; + + NoteComplete(); + return true; +} + +PBackgroundIDBRequestChild* +BackgroundVersionChangeTransactionChild::AllocPBackgroundIDBRequestChild( + const RequestParams& aParams) +{ + MOZ_CRASH("PBackgroundIDBRequestChild actors should be manually " + "constructed!"); +} + +bool +BackgroundVersionChangeTransactionChild::DeallocPBackgroundIDBRequestChild( + PBackgroundIDBRequestChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundRequestChild*>(aActor); + return true; +} + +PBackgroundIDBCursorChild* +BackgroundVersionChangeTransactionChild::AllocPBackgroundIDBCursorChild( + const OpenCursorParams& aParams) +{ + AssertIsOnOwningThread(); + + MOZ_CRASH("PBackgroundIDBCursorChild actors should be manually constructed!"); +} + +bool +BackgroundVersionChangeTransactionChild::DeallocPBackgroundIDBCursorChild( + PBackgroundIDBCursorChild* aActor) +{ + MOZ_ASSERT(aActor); + + delete static_cast<BackgroundCursorChild*>(aActor); + return true; +} + +/******************************************************************************* + * BackgroundMutableFileChild + ******************************************************************************/ + +BackgroundMutableFileChild::BackgroundMutableFileChild( + DEBUGONLY(PRThread* aOwningThread,) + const nsAString& aName, + const nsAString& aType) + : BackgroundMutableFileChildBase(DEBUGONLY(aOwningThread)) + , mName(aName) + , mType(aType) +{ + // Can't assert owning thread here because IPDL has not yet set our manager! + MOZ_COUNT_CTOR(indexedDB::BackgroundMutableFileChild); +} + +BackgroundMutableFileChild::~BackgroundMutableFileChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundMutableFileChild); +} + +already_AddRefed<MutableFileBase> +BackgroundMutableFileChild::CreateMutableFile() +{ + auto database = + static_cast<BackgroundDatabaseChild*>(Manager())->GetDOMObject(); + MOZ_ASSERT(database); + + RefPtr<IDBMutableFile> mutableFile = + new IDBMutableFile(database, this, mName, mType); + + return mutableFile.forget(); +} + +/******************************************************************************* + * BackgroundRequestChild + ******************************************************************************/ + +BackgroundRequestChild::BackgroundRequestChild(IDBRequest* aRequest) + : BackgroundRequestChildBase(aRequest) + , mTransaction(aRequest->GetTransaction()) + , mRunningPreprocessHelpers(0) + , mCurrentModuleSetIndex(0) + , mPreprocessResultCode(NS_OK) + , mGetAll(false) +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundRequestChild); +} + +BackgroundRequestChild::~BackgroundRequestChild() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mTransaction); + + MOZ_COUNT_DTOR(indexedDB::BackgroundRequestChild); +} + +void +BackgroundRequestChild::MaybeSendContinue() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRunningPreprocessHelpers > 0); + + if (--mRunningPreprocessHelpers == 0) { + PreprocessResponse response; + + if (NS_SUCCEEDED(mPreprocessResultCode)) { + if (mGetAll) { + response = ObjectStoreGetAllPreprocessResponse(); + } else { + response = ObjectStoreGetPreprocessResponse(); + } + } else { + response = mPreprocessResultCode; + } + + MOZ_ALWAYS_TRUE(SendContinue(response)); + } +} + +void +BackgroundRequestChild::OnPreprocessFinished( + uint32_t aModuleSetIndex, + nsTArray<RefPtr<JS::WasmModule>>& aModuleSet) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aModuleSetIndex < mPreprocessHelpers.Length()); + MOZ_ASSERT(!aModuleSet.IsEmpty()); + MOZ_ASSERT(mPreprocessHelpers[aModuleSetIndex]); + MOZ_ASSERT(mModuleSets[aModuleSetIndex].IsEmpty()); + + mModuleSets[aModuleSetIndex].SwapElements(aModuleSet); + + MaybeSendContinue(); + + mPreprocessHelpers[aModuleSetIndex] = nullptr; +} + +void +BackgroundRequestChild::OnPreprocessFailed(uint32_t aModuleSetIndex, + nsresult aErrorCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aModuleSetIndex < mPreprocessHelpers.Length()); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + MOZ_ASSERT(mPreprocessHelpers[aModuleSetIndex]); + MOZ_ASSERT(mModuleSets[aModuleSetIndex].IsEmpty()); + + if (NS_SUCCEEDED(mPreprocessResultCode)) { + mPreprocessResultCode = aErrorCode; + } + + MaybeSendContinue(); + + mPreprocessHelpers[aModuleSetIndex] = nullptr; +} + +const nsTArray<RefPtr<JS::WasmModule>>* +BackgroundRequestChild::GetNextModuleSet(const StructuredCloneReadInfo& aInfo) +{ + if (!aInfo.mHasPreprocessInfo) { + return nullptr; + } + + MOZ_ASSERT(mCurrentModuleSetIndex < mModuleSets.Length()); + MOZ_ASSERT(!mModuleSets[mCurrentModuleSetIndex].IsEmpty()); + return &mModuleSets[mCurrentModuleSetIndex++]; +} + +void +BackgroundRequestChild::HandleResponse(nsresult aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResponse)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aResponse) == NS_ERROR_MODULE_DOM_INDEXEDDB); + MOZ_ASSERT(mTransaction); + + DispatchErrorEvent(mRequest, aResponse, mTransaction); +} + +void +BackgroundRequestChild::HandleResponse(const Key& aResponse) +{ + AssertIsOnOwningThread(); + + ResultHelper helper(mRequest, mTransaction, &aResponse); + + DispatchSuccessEvent(&helper); +} + +void +BackgroundRequestChild::HandleResponse(const nsTArray<Key>& aResponse) +{ + AssertIsOnOwningThread(); + + ResultHelper helper(mRequest, mTransaction, &aResponse); + + DispatchSuccessEvent(&helper); +} + +void +BackgroundRequestChild::HandleResponse( + const SerializedStructuredCloneReadInfo& aResponse) +{ + AssertIsOnOwningThread(); + + // XXX Fix this somehow... + auto& serializedCloneInfo = + const_cast<SerializedStructuredCloneReadInfo&>(aResponse); + + StructuredCloneReadInfo cloneReadInfo(Move(serializedCloneInfo)); + + DeserializeStructuredCloneFiles(mTransaction->Database(), + aResponse.files(), + GetNextModuleSet(cloneReadInfo), + cloneReadInfo.mFiles); + + ResultHelper helper(mRequest, mTransaction, &cloneReadInfo); + + DispatchSuccessEvent(&helper); +} + +void +BackgroundRequestChild::HandleResponse( + const nsTArray<SerializedStructuredCloneReadInfo>& aResponse) +{ + AssertIsOnOwningThread(); + + nsTArray<StructuredCloneReadInfo> cloneReadInfos; + + if (!aResponse.IsEmpty()) { + const uint32_t count = aResponse.Length(); + + cloneReadInfos.SetCapacity(count); + + IDBDatabase* database = mTransaction->Database(); + + for (uint32_t index = 0; index < count; index++) { + // XXX Fix this somehow... + auto& serializedCloneInfo = + const_cast<SerializedStructuredCloneReadInfo&>(aResponse[index]); + + StructuredCloneReadInfo* cloneReadInfo = cloneReadInfos.AppendElement(); + + // Move relevant data into the cloneReadInfo + *cloneReadInfo = Move(serializedCloneInfo); + + // Get the files + nsTArray<StructuredCloneFile> files; + DeserializeStructuredCloneFiles(database, + serializedCloneInfo.files(), + GetNextModuleSet(*cloneReadInfo), + files); + + cloneReadInfo->mFiles = Move(files); + } + } + + ResultHelper helper(mRequest, mTransaction, &cloneReadInfos); + + DispatchSuccessEvent(&helper); +} + +void +BackgroundRequestChild::HandleResponse(JS::Handle<JS::Value> aResponse) +{ + AssertIsOnOwningThread(); + + ResultHelper helper(mRequest, mTransaction, &aResponse); + + DispatchSuccessEvent(&helper); +} + +void +BackgroundRequestChild::HandleResponse(uint64_t aResponse) +{ + AssertIsOnOwningThread(); + + JS::Value response(JS::NumberValue(aResponse)); + + ResultHelper helper(mRequest, mTransaction, &response); + + DispatchSuccessEvent(&helper); +} + +nsresult +BackgroundRequestChild::HandlePreprocess( + const WasmModulePreprocessInfo& aPreprocessInfo) +{ + AssertIsOnOwningThread(); + + IDBDatabase* database = mTransaction->Database(); + + mPreprocessHelpers.SetLength(1); + + nsTArray<StructuredCloneFile> files; + DeserializeStructuredCloneFiles(database, + aPreprocessInfo.files(), + nullptr, + files); + + + RefPtr<PreprocessHelper>& preprocessHelper = mPreprocessHelpers[0]; + preprocessHelper = new PreprocessHelper(0, this); + + nsresult rv = preprocessHelper->Init(files); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = preprocessHelper->Dispatch(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mRunningPreprocessHelpers++; + + mModuleSets.SetLength(1); + + return NS_OK; +} + +nsresult +BackgroundRequestChild::HandlePreprocess( + const nsTArray<WasmModulePreprocessInfo>& aPreprocessInfos) +{ + AssertIsOnOwningThread(); + + IDBDatabase* database = mTransaction->Database(); + + uint32_t count = aPreprocessInfos.Length(); + + mPreprocessHelpers.SetLength(count); + + // TODO: Since we use the stream transport service, this can spawn 25 threads + // and has the potential to cause some annoying browser hiccups. + // Consider using a single thread or a very small threadpool. + for (uint32_t index = 0; index < count; index++) { + const WasmModulePreprocessInfo& preprocessInfo = aPreprocessInfos[index]; + + nsTArray<StructuredCloneFile> files; + DeserializeStructuredCloneFiles(database, + preprocessInfo.files(), + nullptr, + files); + + + RefPtr<PreprocessHelper>& preprocessHelper = mPreprocessHelpers[index]; + preprocessHelper = new PreprocessHelper(index, this); + + nsresult rv = preprocessHelper->Init(files); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = preprocessHelper->Dispatch(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mRunningPreprocessHelpers++; + } + + mModuleSets.SetLength(count); + + mGetAll = true; + + return NS_OK; +} + +void +BackgroundRequestChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + for (uint32_t count = mPreprocessHelpers.Length(), index = 0; + index < count; + index++) { + RefPtr<PreprocessHelper>& preprocessHelper = mPreprocessHelpers[index]; + + if (preprocessHelper) { + preprocessHelper->ClearActor(); + + preprocessHelper = nullptr; + } + } + + if (mTransaction) { + mTransaction->AssertIsOnOwningThread(); + + mTransaction->OnRequestFinished(/* aActorDestroyedNormally */ + aWhy == Deletion); +#ifdef DEBUG + mTransaction = nullptr; +#endif + } +} + +bool +BackgroundRequestChild::Recv__delete__(const RequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + + MaybeCollectGarbageOnIPCMessage(); + + if (mTransaction->IsAborted()) { + // Always fire an "error" event with ABORT_ERR if the transaction was + // aborted, even if the request succeeded or failed with another error. + HandleResponse(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } else { + switch (aResponse.type()) { + case RequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case RequestResponse::TObjectStoreAddResponse: + HandleResponse(aResponse.get_ObjectStoreAddResponse().key()); + break; + + case RequestResponse::TObjectStorePutResponse: + HandleResponse(aResponse.get_ObjectStorePutResponse().key()); + break; + + case RequestResponse::TObjectStoreGetResponse: + HandleResponse(aResponse.get_ObjectStoreGetResponse().cloneInfo()); + break; + + case RequestResponse::TObjectStoreGetKeyResponse: + HandleResponse(aResponse.get_ObjectStoreGetKeyResponse().key()); + break; + + case RequestResponse::TObjectStoreGetAllResponse: + HandleResponse(aResponse.get_ObjectStoreGetAllResponse().cloneInfos()); + break; + + case RequestResponse::TObjectStoreGetAllKeysResponse: + HandleResponse(aResponse.get_ObjectStoreGetAllKeysResponse().keys()); + break; + + case RequestResponse::TObjectStoreDeleteResponse: + HandleResponse(JS::UndefinedHandleValue); + break; + + case RequestResponse::TObjectStoreClearResponse: + HandleResponse(JS::UndefinedHandleValue); + break; + + case RequestResponse::TObjectStoreCountResponse: + HandleResponse(aResponse.get_ObjectStoreCountResponse().count()); + break; + + case RequestResponse::TIndexGetResponse: + HandleResponse(aResponse.get_IndexGetResponse().cloneInfo()); + break; + + case RequestResponse::TIndexGetKeyResponse: + HandleResponse(aResponse.get_IndexGetKeyResponse().key()); + break; + + case RequestResponse::TIndexGetAllResponse: + HandleResponse(aResponse.get_IndexGetAllResponse().cloneInfos()); + break; + + case RequestResponse::TIndexGetAllKeysResponse: + HandleResponse(aResponse.get_IndexGetAllKeysResponse().keys()); + break; + + case RequestResponse::TIndexCountResponse: + HandleResponse(aResponse.get_IndexCountResponse().count()); + break; + + default: + MOZ_CRASH("Unknown response type!"); + } + } + + mTransaction->OnRequestFinished(/* aActorDestroyedNormally */ true); + + // Null this out so that we don't try to call OnRequestFinished() again in + // ActorDestroy. + mTransaction = nullptr; + + return true; +} + +bool +BackgroundRequestChild::RecvPreprocess(const PreprocessParams& aParams) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + MaybeCollectGarbageOnIPCMessage(); + + nsresult rv; + + switch (aParams.type()) { + case PreprocessParams::TObjectStoreGetPreprocessParams: { + ObjectStoreGetPreprocessParams params = + aParams.get_ObjectStoreGetPreprocessParams(); + + rv = HandlePreprocess(params.preprocessInfo()); + + break; + } + + case PreprocessParams::TObjectStoreGetAllPreprocessParams: { + ObjectStoreGetAllPreprocessParams params = + aParams.get_ObjectStoreGetAllPreprocessParams(); + + rv = HandlePreprocess(params.preprocessInfos()); + + break; + } + + default: + MOZ_CRASH("Unknown params type!"); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return SendContinue(rv); + } + + return true; +} + +nsresult +BackgroundRequestChild:: +PreprocessHelper::Init(const nsTArray<StructuredCloneFile>& aFiles) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aFiles.IsEmpty()); + + uint32_t count = aFiles.Length(); + + // We should receive even number of files. + MOZ_ASSERT(count % 2 == 0); + + // Let's process it as pairs. + count = count / 2; + + nsTArray<StreamPair> streamPairs; + for (uint32_t index = 0; index < count; index++) { + uint32_t bytecodeIndex = index * 2; + uint32_t compiledIndex = bytecodeIndex + 1; + + const StructuredCloneFile& bytecodeFile = aFiles[bytecodeIndex]; + const StructuredCloneFile& compiledFile = aFiles[compiledIndex]; + + MOZ_ASSERT(bytecodeFile.mType == StructuredCloneFile::eWasmBytecode); + MOZ_ASSERT(bytecodeFile.mBlob); + MOZ_ASSERT(compiledFile.mType == StructuredCloneFile::eWasmCompiled); + MOZ_ASSERT(compiledFile.mBlob); + + ErrorResult errorResult; + + nsCOMPtr<nsIInputStream> bytecodeStream; + bytecodeFile.mBlob->GetInternalStream(getter_AddRefs(bytecodeStream), + errorResult); + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + nsCOMPtr<nsIInputStream> compiledStream; + compiledFile.mBlob->GetInternalStream(getter_AddRefs(compiledStream), + errorResult); + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + streamPairs.AppendElement(StreamPair(bytecodeStream, compiledStream)); + } + + mStreamPairs = Move(streamPairs); + + return NS_OK; +} + +nsresult +BackgroundRequestChild:: +PreprocessHelper::Dispatch() +{ + AssertIsOnOwningThread(); + + // The stream transport service is used for asynchronous processing. It has + // a threadpool with a high cap of 25 threads. Fortunately, the service can + // be used on workers too. + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + nsresult rv = target->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +BackgroundRequestChild:: +PreprocessHelper::RunOnOwningThread() +{ + AssertIsOnOwningThread(); + + if (mActor) { + if (NS_SUCCEEDED(mResultCode)) { + mActor->OnPreprocessFinished(mModuleSetIndex, mModuleSet); + + MOZ_ASSERT(mModuleSet.IsEmpty()); + } else { + mActor->OnPreprocessFailed(mModuleSetIndex, mResultCode); + } + } +} + +nsresult +BackgroundRequestChild:: +PreprocessHelper::RunOnStreamTransportThread() +{ + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(!mStreamPairs.IsEmpty()); + MOZ_ASSERT(mModuleSet.IsEmpty()); + + const uint32_t count = mStreamPairs.Length(); + + for (uint32_t index = 0; index < count; index++) { + const StreamPair& streamPair = mStreamPairs[index]; + + const nsCOMPtr<nsIInputStream>& bytecodeStream = streamPair.first; + + MOZ_ASSERT(bytecodeStream); + + PRFileDesc* bytecodeFileDesc = GetFileDescriptorFromStream(bytecodeStream); + if (NS_WARN_IF(!bytecodeFileDesc)) { + return NS_ERROR_FAILURE; + } + + const nsCOMPtr<nsIInputStream>& compiledStream = streamPair.second; + + MOZ_ASSERT(compiledStream); + + PRFileDesc* compiledFileDesc = GetFileDescriptorFromStream(compiledStream); + if (NS_WARN_IF(!compiledFileDesc)) { + return NS_ERROR_FAILURE; + } + + JS::BuildIdCharVector buildId; + bool ok = GetBuildId(&buildId); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_FAILURE; + } + + RefPtr<JS::WasmModule> module = JS::DeserializeWasmModule(bytecodeFileDesc, + compiledFileDesc, + Move(buildId), + nullptr, + 0, + 0); + if (NS_WARN_IF(!module)) { + return NS_ERROR_FAILURE; + } + + mModuleSet.AppendElement(module); + } + + mStreamPairs.Clear(); + + return NS_OK; +} + +NS_IMETHODIMP +BackgroundRequestChild:: +PreprocessHelper::Run() +{ + if (IsOnOwningThread()) { + RunOnOwningThread(); + } else { + nsresult rv = RunOnStreamTransportThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ASSERT(mResultCode == NS_OK); + mResultCode = rv; + } + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + return NS_OK; +} + +nsresult +BackgroundRequestChild:: +PreprocessHelper::Cancel() +{ + return NS_OK; +} + +/******************************************************************************* + * BackgroundCursorChild + ******************************************************************************/ + +// Does not need to be threadsafe since this only runs on one thread, but +// inheriting from CancelableRunnable is easy. +class BackgroundCursorChild::DelayedActionRunnable final + : public CancelableRunnable +{ + using ActionFunc = void (BackgroundCursorChild::*)(); + + BackgroundCursorChild* mActor; + RefPtr<IDBRequest> mRequest; + ActionFunc mActionFunc; + +public: + explicit + DelayedActionRunnable(BackgroundCursorChild* aActor, ActionFunc aActionFunc) + : mActor(aActor) + , mRequest(aActor->mRequest) + , mActionFunc(aActionFunc) + { + MOZ_ASSERT(aActor); + aActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mActionFunc); + } + +private: + ~DelayedActionRunnable() + { } + + NS_DECL_NSIRUNNABLE + nsresult Cancel() override; +}; + +BackgroundCursorChild::BackgroundCursorChild(IDBRequest* aRequest, + IDBObjectStore* aObjectStore, + Direction aDirection) + : mRequest(aRequest) + , mTransaction(aRequest->GetTransaction()) + , mObjectStore(aObjectStore) + , mIndex(nullptr) + , mCursor(nullptr) + , mStrongRequest(aRequest) + , mDirection(aDirection) +{ + MOZ_ASSERT(aObjectStore); + aObjectStore->AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + MOZ_COUNT_CTOR(indexedDB::BackgroundCursorChild); + +#ifdef DEBUG + mOwningThread = PR_GetCurrentThread(); + MOZ_ASSERT(mOwningThread); +#endif +} + +BackgroundCursorChild::BackgroundCursorChild(IDBRequest* aRequest, + IDBIndex* aIndex, + Direction aDirection) + : mRequest(aRequest) + , mTransaction(aRequest->GetTransaction()) + , mObjectStore(nullptr) + , mIndex(aIndex) + , mCursor(nullptr) + , mStrongRequest(aRequest) + , mDirection(aDirection) +{ + MOZ_ASSERT(aIndex); + aIndex->AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + MOZ_COUNT_CTOR(indexedDB::BackgroundCursorChild); + +#ifdef DEBUG + mOwningThread = PR_GetCurrentThread(); + MOZ_ASSERT(mOwningThread); +#endif +} + +BackgroundCursorChild::~BackgroundCursorChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundCursorChild); +} + +#ifdef DEBUG + +void +BackgroundCursorChild::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mOwningThread == PR_GetCurrentThread()); +} + +#endif // DEBUG + +void +BackgroundCursorChild::SendContinueInternal(const CursorRequestParams& aParams) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + // Make sure all our DOM objects stay alive. + mStrongCursor = mCursor; + + MOZ_ASSERT(mRequest->ReadyState() == IDBRequestReadyState::Done); + mRequest->Reset(); + + mTransaction->OnNewRequest(); + + MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendContinue(aParams)); +} + +void +BackgroundCursorChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + mRequest = nullptr; + mTransaction = nullptr; + mObjectStore = nullptr; + mIndex = nullptr; + + if (mCursor) { + mCursor->ClearBackgroundActor(); + mCursor = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendDeleteMe()); + } +} + +void +BackgroundCursorChild::HandleResponse(nsresult aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResponse)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aResponse) == NS_ERROR_MODULE_DOM_INDEXEDDB); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + DispatchErrorEvent(mRequest, aResponse, mTransaction); +} + +void +BackgroundCursorChild::HandleResponse(const void_t& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + if (mCursor) { + mCursor->Reset(); + } + + ResultHelper helper(mRequest, mTransaction, &JS::NullHandleValue); + DispatchSuccessEvent(&helper); + + if (!mCursor) { + nsCOMPtr<nsIRunnable> deleteRunnable = new DelayedActionRunnable( + this, &BackgroundCursorChild::SendDeleteMeInternal); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(deleteRunnable)); + } +} + +void +BackgroundCursorChild::HandleResponse( + const nsTArray<ObjectStoreCursorResponse>& aResponses) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mObjectStore); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + MOZ_ASSERT(aResponses.Length() == 1); + + // XXX Fix this somehow... + auto& responses = + const_cast<nsTArray<ObjectStoreCursorResponse>&>(aResponses); + + for (ObjectStoreCursorResponse& response : responses) { + StructuredCloneReadInfo cloneReadInfo(Move(response.cloneInfo())); + cloneReadInfo.mDatabase = mTransaction->Database(); + + DeserializeStructuredCloneFiles(mTransaction->Database(), + response.cloneInfo().files(), + nullptr, + cloneReadInfo.mFiles); + + RefPtr<IDBCursor> newCursor; + + if (mCursor) { + mCursor->Reset(Move(response.key()), Move(cloneReadInfo)); + } else { + newCursor = IDBCursor::Create(this, + Move(response.key()), + Move(cloneReadInfo)); + mCursor = newCursor; + } + } + + ResultHelper helper(mRequest, mTransaction, mCursor); + DispatchSuccessEvent(&helper); +} + +void +BackgroundCursorChild::HandleResponse( + const ObjectStoreKeyCursorResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mObjectStore); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + // XXX Fix this somehow... + auto& response = const_cast<ObjectStoreKeyCursorResponse&>(aResponse); + + RefPtr<IDBCursor> newCursor; + + if (mCursor) { + mCursor->Reset(Move(response.key())); + } else { + newCursor = IDBCursor::Create(this, Move(response.key())); + mCursor = newCursor; + } + + ResultHelper helper(mRequest, mTransaction, mCursor); + DispatchSuccessEvent(&helper); +} + +void +BackgroundCursorChild::HandleResponse(const IndexCursorResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mIndex); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + // XXX Fix this somehow... + auto& response = const_cast<IndexCursorResponse&>(aResponse); + + StructuredCloneReadInfo cloneReadInfo(Move(response.cloneInfo())); + cloneReadInfo.mDatabase = mTransaction->Database(); + + DeserializeStructuredCloneFiles(mTransaction->Database(), + aResponse.cloneInfo().files(), + nullptr, + cloneReadInfo.mFiles); + + RefPtr<IDBCursor> newCursor; + + if (mCursor) { + mCursor->Reset(Move(response.key()), + Move(response.sortKey()), + Move(response.objectKey()), + Move(cloneReadInfo)); + } else { + newCursor = IDBCursor::Create(this, + Move(response.key()), + Move(response.sortKey()), + Move(response.objectKey()), + Move(cloneReadInfo)); + mCursor = newCursor; + } + + ResultHelper helper(mRequest, mTransaction, mCursor); + DispatchSuccessEvent(&helper); +} + +void +BackgroundCursorChild::HandleResponse(const IndexKeyCursorResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mIndex); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + // XXX Fix this somehow... + auto& response = const_cast<IndexKeyCursorResponse&>(aResponse); + + RefPtr<IDBCursor> newCursor; + + if (mCursor) { + mCursor->Reset(Move(response.key()), + Move(response.sortKey()), + Move(response.objectKey())); + } else { + newCursor = IDBCursor::Create(this, + Move(response.key()), + Move(response.sortKey()), + Move(response.objectKey())); + mCursor = newCursor; + } + + ResultHelper helper(mRequest, mTransaction, mCursor); + DispatchSuccessEvent(&helper); +} + +void +BackgroundCursorChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(aWhy == Deletion, !mStrongRequest); + MOZ_ASSERT_IF(aWhy == Deletion, !mStrongCursor); + + MaybeCollectGarbageOnIPCMessage(); + + if (mStrongRequest && !mStrongCursor && mTransaction) { + mTransaction->OnRequestFinished(/* aActorDestroyedNormally */ + aWhy == Deletion); + } + + if (mCursor) { + mCursor->ClearBackgroundActor(); +#ifdef DEBUG + mCursor = nullptr; +#endif + } + +#ifdef DEBUG + mRequest = nullptr; + mTransaction = nullptr; + mObjectStore = nullptr; + mIndex = nullptr; +#endif +} + +bool +BackgroundCursorChild::RecvResponse(const CursorResponse& aResponse) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aResponse.type() != CursorResponse::T__None); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT_IF(mCursor, mStrongCursor); + MOZ_ASSERT_IF(!mCursor, mStrongRequest); + + MaybeCollectGarbageOnIPCMessage(); + + RefPtr<IDBRequest> request; + mStrongRequest.swap(request); + + RefPtr<IDBCursor> cursor; + mStrongCursor.swap(cursor); + + switch (aResponse.type()) { + case CursorResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case CursorResponse::Tvoid_t: + HandleResponse(aResponse.get_void_t()); + break; + + case CursorResponse::TArrayOfObjectStoreCursorResponse: + HandleResponse(aResponse.get_ArrayOfObjectStoreCursorResponse()); + break; + + case CursorResponse::TObjectStoreKeyCursorResponse: + HandleResponse(aResponse.get_ObjectStoreKeyCursorResponse()); + break; + + case CursorResponse::TIndexCursorResponse: + HandleResponse(aResponse.get_IndexCursorResponse()); + break; + + case CursorResponse::TIndexKeyCursorResponse: + HandleResponse(aResponse.get_IndexKeyCursorResponse()); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + mTransaction->OnRequestFinished(/* aActorDestroyedNormally */ true); + + return true; +} + +NS_IMETHODIMP +BackgroundCursorChild:: +DelayedActionRunnable::Run() +{ + MOZ_ASSERT(mActor); + mActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mActionFunc); + + (mActor->*mActionFunc)(); + + mActor = nullptr; + mRequest = nullptr; + + return NS_OK; +} + +nsresult +BackgroundCursorChild:: +DelayedActionRunnable::Cancel() +{ + if (NS_WARN_IF(!mActor)) { + return NS_ERROR_UNEXPECTED; + } + + // This must always run to clean up our state. + Run(); + + return NS_OK; +} + +/******************************************************************************* + * BackgroundUtilsChild + ******************************************************************************/ + +BackgroundUtilsChild::BackgroundUtilsChild(IndexedDatabaseManager* aManager) + : mManager(aManager) +#ifdef DEBUG + , mOwningThread(NS_GetCurrentThread()) +#endif +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aManager); + + MOZ_COUNT_CTOR(indexedDB::BackgroundUtilsChild); +} + +BackgroundUtilsChild::~BackgroundUtilsChild() +{ + MOZ_COUNT_DTOR(indexedDB::BackgroundUtilsChild); +} + +#ifdef DEBUG + +void +BackgroundUtilsChild::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mOwningThread); + + bool current; + MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t))); + MOZ_ASSERT(current); +} + +#endif // DEBUG + +void +BackgroundUtilsChild::SendDeleteMeInternal() +{ + AssertIsOnOwningThread(); + + if (mManager) { + mManager->ClearBackgroundActor(); + mManager = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundIndexedDBUtilsChild::SendDeleteMe()); + } +} + +void +BackgroundUtilsChild::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + if (mManager) { + mManager->ClearBackgroundActor(); +#ifdef DEBUG + mManager = nullptr; +#endif + } +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/ActorsChild.h b/dom/indexedDB/ActorsChild.h new file mode 100644 index 000000000..58059787c --- /dev/null +++ b/dom/indexedDB/ActorsChild.h @@ -0,0 +1,877 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_actorschild_h__ +#define mozilla_dom_indexeddb_actorschild_h__ + +#include "IDBTransaction.h" +#include "js/RootingAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/filehandle/ActorsChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBCursorChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseRequestChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBRequestChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBTransactionChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsChild.h" +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +class nsIEventTarget; +struct nsID; +struct PRThread; + +namespace JS { +struct WasmModule; +} // namespace JS + +namespace mozilla { +namespace ipc { + +class BackgroundChildImpl; + +} // namespace ipc + +namespace dom { + +class IDBCursor; +class IDBDatabase; +class IDBFactory; +class IDBMutableFile; +class IDBOpenDBRequest; +class IDBRequest; +class IndexedDatabaseManager; + +namespace indexedDB { + +class Key; +class PermissionRequestChild; +class PermissionRequestParent; +class SerializedStructuredCloneReadInfo; + +class ThreadLocal +{ + friend class nsAutoPtr<ThreadLocal>; + friend IDBFactory; + + LoggingInfo mLoggingInfo; + IDBTransaction* mCurrentTransaction; + nsCString mLoggingIdString; + +#ifdef DEBUG + PRThread* mOwningThread; +#endif + +public: + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + const LoggingInfo& + GetLoggingInfo() const + { + AssertIsOnOwningThread(); + + return mLoggingInfo; + } + + const nsID& + Id() const + { + AssertIsOnOwningThread(); + + return mLoggingInfo.backgroundChildLoggingId(); + } + + const nsCString& + IdString() const + { + AssertIsOnOwningThread(); + + return mLoggingIdString; + } + + int64_t + NextTransactionSN(IDBTransaction::Mode aMode) + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mLoggingInfo.nextTransactionSerialNumber() < INT64_MAX); + MOZ_ASSERT(mLoggingInfo.nextVersionChangeTransactionSerialNumber() > + INT64_MIN); + + if (aMode == IDBTransaction::VERSION_CHANGE) { + return mLoggingInfo.nextVersionChangeTransactionSerialNumber()--; + } + + return mLoggingInfo.nextTransactionSerialNumber()++; + } + + uint64_t + NextRequestSN() + { + AssertIsOnOwningThread(); + MOZ_ASSERT(mLoggingInfo.nextRequestSerialNumber() < UINT64_MAX); + + return mLoggingInfo.nextRequestSerialNumber()++; + } + + void + SetCurrentTransaction(IDBTransaction* aCurrentTransaction) + { + AssertIsOnOwningThread(); + + mCurrentTransaction = aCurrentTransaction; + } + + IDBTransaction* + GetCurrentTransaction() const + { + AssertIsOnOwningThread(); + + return mCurrentTransaction; + } + +private: + explicit ThreadLocal(const nsID& aBackgroundChildLoggingId); + ~ThreadLocal(); + + ThreadLocal() = delete; + ThreadLocal(const ThreadLocal& aOther) = delete; +}; + +class BackgroundFactoryChild final + : public PBackgroundIDBFactoryChild +{ + friend class mozilla::ipc::BackgroundChildImpl; + friend IDBFactory; + + IDBFactory* mFactory; + +#ifdef DEBUG + nsCOMPtr<nsIEventTarget> mOwningThread; +#endif + +public: +#ifdef DEBUG + void + AssertIsOnOwningThread() const; + + nsIEventTarget* + OwningThread() const; +#else + void + AssertIsOnOwningThread() const + { } +#endif + + IDBFactory* + GetDOMObject() const + { + AssertIsOnOwningThread(); + return mFactory; + } + +private: + // Only created by IDBFactory. + explicit BackgroundFactoryChild(IDBFactory* aFactory); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~BackgroundFactoryChild(); + + void + SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PBackgroundIDBFactoryRequestChild* + AllocPBackgroundIDBFactoryRequestChild(const FactoryRequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBFactoryRequestChild( + PBackgroundIDBFactoryRequestChild* aActor) + override; + + virtual PBackgroundIDBDatabaseChild* + AllocPBackgroundIDBDatabaseChild(const DatabaseSpec& aSpec, + PBackgroundIDBFactoryRequestChild* aRequest) + override; + + virtual bool + DeallocPBackgroundIDBDatabaseChild(PBackgroundIDBDatabaseChild* aActor) + override; + + bool + SendDeleteMe() = delete; +}; + +class BackgroundDatabaseChild; + +class BackgroundRequestChildBase +{ +protected: + RefPtr<IDBRequest> mRequest; + +public: + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + +protected: + explicit BackgroundRequestChildBase(IDBRequest* aRequest); + + virtual + ~BackgroundRequestChildBase(); +}; + +class BackgroundFactoryRequestChild final + : public BackgroundRequestChildBase + , public PBackgroundIDBFactoryRequestChild +{ + typedef mozilla::dom::quota::PersistenceType PersistenceType; + + friend IDBFactory; + friend class BackgroundFactoryChild; + friend class BackgroundDatabaseChild; + friend class PermissionRequestChild; + friend class PermissionRequestParent; + + RefPtr<IDBFactory> mFactory; + const uint64_t mRequestedVersion; + const bool mIsDeleteOp; + +public: + IDBOpenDBRequest* + GetOpenDBRequest() const; + +private: + // Only created by IDBFactory. + BackgroundFactoryRequestChild(IDBFactory* aFactory, + IDBOpenDBRequest* aOpenRequest, + bool aIsDeleteOp, + uint64_t aRequestedVersion); + + // Only destroyed by BackgroundFactoryChild. + ~BackgroundFactoryRequestChild(); + + bool + HandleResponse(nsresult aResponse); + + bool + HandleResponse(const OpenDatabaseRequestResponse& aResponse); + + bool + HandleResponse(const DeleteDatabaseRequestResponse& aResponse); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + Recv__delete__(const FactoryRequestResponse& aResponse) override; + + virtual bool + RecvPermissionChallenge(const PrincipalInfo& aPrincipalInfo) override; + + virtual bool + RecvBlocked(const uint64_t& aCurrentVersion) override; +}; + +class BackgroundDatabaseChild final + : public PBackgroundIDBDatabaseChild +{ + friend class BackgroundFactoryChild; + friend class BackgroundFactoryRequestChild; + friend IDBDatabase; + + nsAutoPtr<DatabaseSpec> mSpec; + RefPtr<IDBDatabase> mTemporaryStrongDatabase; + BackgroundFactoryRequestChild* mOpenRequestActor; + IDBDatabase* mDatabase; + +public: + void + AssertIsOnOwningThread() const + { + static_cast<BackgroundFactoryChild*>(Manager())->AssertIsOnOwningThread(); + } + +#ifdef DEBUG + nsIEventTarget* + OwningThread() const + { + return static_cast<BackgroundFactoryChild*>(Manager())->OwningThread(); + } +#endif + + const DatabaseSpec* + Spec() const + { + AssertIsOnOwningThread(); + return mSpec; + } + + IDBDatabase* + GetDOMObject() const + { + AssertIsOnOwningThread(); + return mDatabase; + } + +private: + // Only constructed by BackgroundFactoryChild. + BackgroundDatabaseChild(const DatabaseSpec& aSpec, + BackgroundFactoryRequestChild* aOpenRequest); + + // Only destroyed by BackgroundFactoryChild. + ~BackgroundDatabaseChild(); + + void + SendDeleteMeInternal(); + + void + EnsureDOMObject(); + + void + ReleaseDOMObject(); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PBackgroundIDBDatabaseFileChild* + AllocPBackgroundIDBDatabaseFileChild(PBlobChild* aBlobChild) + override; + + virtual bool + DeallocPBackgroundIDBDatabaseFileChild( + PBackgroundIDBDatabaseFileChild* aActor) + override; + + virtual PBackgroundIDBDatabaseRequestChild* + AllocPBackgroundIDBDatabaseRequestChild(const DatabaseRequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBDatabaseRequestChild( + PBackgroundIDBDatabaseRequestChild* aActor) + override; + + virtual PBackgroundIDBTransactionChild* + AllocPBackgroundIDBTransactionChild( + const nsTArray<nsString>& aObjectStoreNames, + const Mode& aMode) + override; + + virtual bool + DeallocPBackgroundIDBTransactionChild(PBackgroundIDBTransactionChild* aActor) + override; + + virtual PBackgroundIDBVersionChangeTransactionChild* + AllocPBackgroundIDBVersionChangeTransactionChild( + const uint64_t& aCurrentVersion, + const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, + const int64_t& aNextIndexId) + override; + + virtual bool + RecvPBackgroundIDBVersionChangeTransactionConstructor( + PBackgroundIDBVersionChangeTransactionChild* aActor, + const uint64_t& aCurrentVersion, + const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, + const int64_t& aNextIndexId) + override; + + virtual bool + DeallocPBackgroundIDBVersionChangeTransactionChild( + PBackgroundIDBVersionChangeTransactionChild* aActor) + override; + + virtual PBackgroundMutableFileChild* + AllocPBackgroundMutableFileChild(const nsString& aName, + const nsString& aType) override; + + virtual bool + DeallocPBackgroundMutableFileChild(PBackgroundMutableFileChild* aActor) + override; + + virtual bool + RecvVersionChange(const uint64_t& aOldVersion, + const NullableVersion& aNewVersion) + override; + + virtual bool + RecvInvalidate() override; + + virtual bool + RecvCloseAfterInvalidationComplete() override; + + bool + SendDeleteMe() = delete; +}; + +class BackgroundDatabaseRequestChild final + : public BackgroundRequestChildBase + , public PBackgroundIDBDatabaseRequestChild +{ + friend class BackgroundDatabaseChild; + friend IDBDatabase; + + RefPtr<IDBDatabase> mDatabase; + +private: + // Only created by IDBDatabase. + BackgroundDatabaseRequestChild(IDBDatabase* aDatabase, + IDBRequest* aRequest); + + // Only destroyed by BackgroundDatabaseChild. + ~BackgroundDatabaseRequestChild(); + + bool + HandleResponse(nsresult aResponse); + + bool + HandleResponse(const CreateFileRequestResponse& aResponse); + + // IPDL methods are only called by IPDL. + virtual bool + Recv__delete__(const DatabaseRequestResponse& aResponse) override; +}; + +class BackgroundVersionChangeTransactionChild; + +class BackgroundTransactionBase +{ + friend class BackgroundVersionChangeTransactionChild; + + // mTemporaryStrongTransaction is strong and is only valid until the end of + // NoteComplete() member function or until the NoteActorDestroyed() member + // function is called. + RefPtr<IDBTransaction> mTemporaryStrongTransaction; + +protected: + // mTransaction is weak and is valid until the NoteActorDestroyed() member + // function is called. + IDBTransaction* mTransaction; + +public: +#ifdef DEBUG + virtual void + AssertIsOnOwningThread() const = 0; +#else + void + AssertIsOnOwningThread() const + { } +#endif + + IDBTransaction* + GetDOMObject() const + { + AssertIsOnOwningThread(); + return mTransaction; + } + +protected: + BackgroundTransactionBase(); + explicit BackgroundTransactionBase(IDBTransaction* aTransaction); + + virtual + ~BackgroundTransactionBase(); + + void + NoteActorDestroyed(); + + void + NoteComplete(); + +private: + // Only called by BackgroundVersionChangeTransactionChild. + void + SetDOMTransaction(IDBTransaction* aDOMObject); +}; + +class BackgroundTransactionChild final + : public BackgroundTransactionBase + , public PBackgroundIDBTransactionChild +{ + friend class BackgroundDatabaseChild; + friend IDBDatabase; + +public: +#ifdef DEBUG + virtual void + AssertIsOnOwningThread() const override; +#endif + + void + SendDeleteMeInternal(); + +private: + // Only created by IDBDatabase. + explicit BackgroundTransactionChild(IDBTransaction* aTransaction); + + // Only destroyed by BackgroundDatabaseChild. + ~BackgroundTransactionChild(); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + bool + RecvComplete(const nsresult& aResult) override; + + virtual PBackgroundIDBRequestChild* + AllocPBackgroundIDBRequestChild(const RequestParams& aParams) override; + + virtual bool + DeallocPBackgroundIDBRequestChild(PBackgroundIDBRequestChild* aActor) + override; + + virtual PBackgroundIDBCursorChild* + AllocPBackgroundIDBCursorChild(const OpenCursorParams& aParams) override; + + virtual bool + DeallocPBackgroundIDBCursorChild(PBackgroundIDBCursorChild* aActor) + override; + + bool + SendDeleteMe() = delete; +}; + +class BackgroundVersionChangeTransactionChild final + : public BackgroundTransactionBase + , public PBackgroundIDBVersionChangeTransactionChild +{ + friend class BackgroundDatabaseChild; + + IDBOpenDBRequest* mOpenDBRequest; + +public: +#ifdef DEBUG + virtual void + AssertIsOnOwningThread() const override; +#endif + + void + SendDeleteMeInternal(bool aFailedConstructor); + +private: + // Only created by BackgroundDatabaseChild. + explicit BackgroundVersionChangeTransactionChild(IDBOpenDBRequest* aOpenDBRequest); + + // Only destroyed by BackgroundDatabaseChild. + ~BackgroundVersionChangeTransactionChild(); + + // Only called by BackgroundDatabaseChild. + void + SetDOMTransaction(IDBTransaction* aDOMObject) + { + BackgroundTransactionBase::SetDOMTransaction(aDOMObject); + } + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + bool + RecvComplete(const nsresult& aResult) override; + + virtual PBackgroundIDBRequestChild* + AllocPBackgroundIDBRequestChild(const RequestParams& aParams) override; + + virtual bool + DeallocPBackgroundIDBRequestChild(PBackgroundIDBRequestChild* aActor) + override; + + virtual PBackgroundIDBCursorChild* + AllocPBackgroundIDBCursorChild(const OpenCursorParams& aParams) override; + + virtual bool + DeallocPBackgroundIDBCursorChild(PBackgroundIDBCursorChild* aActor) + override; + + bool + SendDeleteMe() = delete; +}; + +class BackgroundMutableFileChild final + : public mozilla::dom::BackgroundMutableFileChildBase +{ + friend class BackgroundDatabaseChild; + + nsString mName; + nsString mType; + +private: + // Only constructed by BackgroundDatabaseChild. + BackgroundMutableFileChild(DEBUGONLY(PRThread* aOwningThread,) + const nsAString& aName, + const nsAString& aType); + + // Only destroyed by BackgroundDatabaseChild. + ~BackgroundMutableFileChild(); + + // BackgroundMutableFileChildBase + virtual already_AddRefed<MutableFileBase> + CreateMutableFile() override; +}; + +class BackgroundRequestChild final + : public BackgroundRequestChildBase + , public PBackgroundIDBRequestChild +{ + friend class BackgroundTransactionChild; + friend class BackgroundVersionChangeTransactionChild; + friend IDBTransaction; + + class PreprocessHelper; + + RefPtr<IDBTransaction> mTransaction; + nsTArray<RefPtr<PreprocessHelper>> mPreprocessHelpers; + nsTArray<nsTArray<RefPtr<JS::WasmModule>>> mModuleSets; + uint32_t mRunningPreprocessHelpers; + uint32_t mCurrentModuleSetIndex; + nsresult mPreprocessResultCode; + bool mGetAll; + +private: + // Only created by IDBTransaction. + explicit + BackgroundRequestChild(IDBRequest* aRequest); + + // Only destroyed by BackgroundTransactionChild or + // BackgroundVersionChangeTransactionChild. + ~BackgroundRequestChild(); + + void + MaybeSendContinue(); + + void + OnPreprocessFinished(uint32_t aModuleSetIndex, + nsTArray<RefPtr<JS::WasmModule>>& aModuleSet); + + void + OnPreprocessFailed(uint32_t aModuleSetIndex, nsresult aErrorCode); + + const nsTArray<RefPtr<JS::WasmModule>>* + GetNextModuleSet(const StructuredCloneReadInfo& aInfo); + + void + HandleResponse(nsresult aResponse); + + void + HandleResponse(const Key& aResponse); + + void + HandleResponse(const nsTArray<Key>& aResponse); + + void + HandleResponse(const SerializedStructuredCloneReadInfo& aResponse); + + void + HandleResponse(const nsTArray<SerializedStructuredCloneReadInfo>& aResponse); + + void + HandleResponse(JS::Handle<JS::Value> aResponse); + + void + HandleResponse(uint64_t aResponse); + + nsresult + HandlePreprocess(const WasmModulePreprocessInfo& aPreprocessInfo); + + nsresult + HandlePreprocess(const nsTArray<WasmModulePreprocessInfo>& aPreprocessInfos); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + Recv__delete__(const RequestResponse& aResponse) override; + + virtual bool + RecvPreprocess(const PreprocessParams& aParams) override; +}; + +class BackgroundCursorChild final + : public PBackgroundIDBCursorChild +{ + friend class BackgroundTransactionChild; + friend class BackgroundVersionChangeTransactionChild; + + class DelayedActionRunnable; + + IDBRequest* mRequest; + IDBTransaction* mTransaction; + IDBObjectStore* mObjectStore; + IDBIndex* mIndex; + IDBCursor* mCursor; + + // These are only set while a request is in progress. + RefPtr<IDBRequest> mStrongRequest; + RefPtr<IDBCursor> mStrongCursor; + + Direction mDirection; + +#ifdef DEBUG + PRThread* mOwningThread; +#endif + +public: + BackgroundCursorChild(IDBRequest* aRequest, + IDBObjectStore* aObjectStore, + Direction aDirection); + + BackgroundCursorChild(IDBRequest* aRequest, + IDBIndex* aIndex, + Direction aDirection); + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + void + SendContinueInternal(const CursorRequestParams& aParams); + + void + SendDeleteMeInternal(); + + IDBRequest* + GetRequest() const + { + AssertIsOnOwningThread(); + + return mRequest; + } + + IDBObjectStore* + GetObjectStore() const + { + AssertIsOnOwningThread(); + + return mObjectStore; + } + + IDBIndex* + GetIndex() const + { + AssertIsOnOwningThread(); + + return mIndex; + } + + Direction + GetDirection() const + { + AssertIsOnOwningThread(); + + return mDirection; + } + +private: + // Only destroyed by BackgroundTransactionChild or + // BackgroundVersionChangeTransactionChild. + ~BackgroundCursorChild(); + + void + HandleResponse(nsresult aResponse); + + void + HandleResponse(const void_t& aResponse); + + void + HandleResponse(const nsTArray<ObjectStoreCursorResponse>& aResponse); + + void + HandleResponse(const ObjectStoreKeyCursorResponse& aResponse); + + void + HandleResponse(const IndexCursorResponse& aResponse); + + void + HandleResponse(const IndexKeyCursorResponse& aResponse); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvResponse(const CursorResponse& aResponse) override; + + // Force callers to use SendContinueInternal. + bool + SendContinue(const CursorRequestParams& aParams) = delete; + + bool + SendDeleteMe() = delete; +}; + +class BackgroundUtilsChild final + : public PBackgroundIndexedDBUtilsChild +{ + friend class mozilla::ipc::BackgroundChildImpl; + friend IndexedDatabaseManager; + + IndexedDatabaseManager* mManager; + +#ifdef DEBUG + nsCOMPtr<nsIEventTarget> mOwningThread; +#endif + +public: + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + +private: + // Only created by IndexedDatabaseManager. + explicit BackgroundUtilsChild(IndexedDatabaseManager* aManager); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~BackgroundUtilsChild(); + + void + SendDeleteMeInternal(); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + bool + SendDeleteMe() = delete; +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_actorschild_h__ diff --git a/dom/indexedDB/ActorsParent.cpp b/dom/indexedDB/ActorsParent.cpp new file mode 100644 index 000000000..702d5c985 --- /dev/null +++ b/dom/indexedDB/ActorsParent.cpp @@ -0,0 +1,29795 @@ +/* -*- Mode: C++; tab-width: 2; 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 "ActorsParent.h" + +#include <algorithm> +#include "FileInfo.h" +#include "FileManager.h" +#include "IDBObjectStore.h" +#include "IDBTransaction.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDatabaseManager.h" +#include "js/StructuredClone.h" +#include "js/Value.h" +#include "jsapi.h" +#include "KeyPath.h" +#include "mozilla/Attributes.h" +#include "mozilla/AppProcessChecker.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Casting.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Maybe.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/SnappyCompressOutputStream.h" +#include "mozilla/SnappyUncompressInputStream.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/storage.h" +#include "mozilla/Unused.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/StructuredCloneTags.h" +#include "mozilla/dom/TabParent.h" +#include "mozilla/dom/filehandle/ActorsParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseRequestParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h" +#include "mozilla/dom/indexedDB/PIndexedDBPermissionRequestParent.h" +#include "mozilla/dom/ipc/BlobParent.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/FileStreams.h" +#include "mozilla/dom/quota/OriginScope.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/ipc/PBackground.h" +#include "mozilla/Scoped.h" +#include "mozilla/storage/Variant.h" +#include "nsAutoPtr.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsClassHashtable.h" +#include "nsCOMPtr.h" +#include "nsDataHashtable.h" +#include "nsEscape.h" +#include "nsHashKeys.h" +#include "nsNetUtil.h" +#include "nsISimpleEnumerator.h" +#include "nsIAppsService.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIFileProtocolHandler.h" +#include "nsIInputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsInterfaceHashtable.h" +#include "nsIOutputStream.h" +#include "nsIPipe.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsISupports.h" +#include "nsISupportsImpl.h" +#include "nsISupportsPriority.h" +#include "nsIThread.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsQueryObject.h" +#include "nsRefPtrHashtable.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsStringStream.h" +#include "nsThreadPool.h" +#include "nsThreadUtils.h" +#include "nsXPCOMCID.h" +#include "PermissionRequestBase.h" +#include "ProfilerHelpers.h" +#include "prsystem.h" +#include "prtime.h" +#include "ReportInternalError.h" +#include "snappy/snappy.h" + +#define DISABLE_ASSERTS_FOR_FUZZING 0 + +#if DISABLE_ASSERTS_FOR_FUZZING +#define ASSERT_UNLESS_FUZZING(...) do { } while (0) +#else +#define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__) +#endif + +#define IDB_DEBUG_LOG(_args) \ + MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), \ + LogLevel::Debug, \ + _args ) + +#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) +#define IDB_MOBILE +#endif + +#define BLOB_IMPL_STORED_FILE_IID \ + {0x6b505c84, 0x2c60, 0x4ffb, {0x8b, 0x91, 0xfe, 0x22, 0xb1, 0xec, 0x75, 0xe2}} + +namespace mozilla { + +MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, + PRFileDesc, + PR_Close); + +namespace dom { +namespace indexedDB { + +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; + +namespace { + +class ConnectionPool; +class Cursor; +class Database; +struct DatabaseActorInfo; +class DatabaseFile; +class DatabaseLoggingInfo; +class DatabaseMaintenance; +class Factory; +class Maintenance; +class MutableFile; +class OpenDatabaseOp; +class TransactionBase; +class TransactionDatabaseOperationBase; +class VersionChangeTransaction; + +/******************************************************************************* + * Constants + ******************************************************************************/ + +// If JS_STRUCTURED_CLONE_VERSION changes then we need to update our major +// schema version. +static_assert(JS_STRUCTURED_CLONE_VERSION == 8, + "Need to update the major schema version."); + +// Major schema version. Bump for almost everything. +const uint32_t kMajorSchemaVersion = 25; + +// Minor schema version. Should almost always be 0 (maybe bump on release +// branches if we have to). +const uint32_t kMinorSchemaVersion = 0; + +// The schema version we store in the SQLite database is a (signed) 32-bit +// integer. The major version is left-shifted 4 bits so the max value is +// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF. +static_assert(kMajorSchemaVersion <= 0xFFFFFFF, + "Major version needs to fit in 28 bits."); +static_assert(kMinorSchemaVersion <= 0xF, + "Minor version needs to fit in 4 bits."); + +const int32_t kSQLiteSchemaVersion = + int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion); + +const int32_t kStorageProgressGranularity = 1000; + +// Changing the value here will override the page size of new databases only. +// A journal mode change and VACUUM are needed to change existing databases, so +// the best way to do that is to use the schema version upgrade mechanism. +const uint32_t kSQLitePageSizeOverride = +#ifdef IDB_MOBILE + 2048; +#else + 4096; +#endif + +static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 || + (kSQLitePageSizeOverride % 2 == 0 && + kSQLitePageSizeOverride >= 512 && + kSQLitePageSizeOverride <= 65536), + "Must be 0 (disabled) or a power of 2 between 512 and 65536!"); + +// Set to -1 to use SQLite's default, 0 to disable, or some positive number to +// enforce a custom limit. +const int32_t kMaxWALPages = 5000; // 20MB on desktop, 10MB on mobile. + +// Set to some multiple of the page size to grow the database in larger chunks. +const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2; + +static_assert(kSQLiteGrowthIncrement >= 0 && + kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 && + kSQLiteGrowthIncrement < uint32_t(INT32_MAX), + "Must be 0 (disabled) or a positive multiple of the page size!"); + +// The maximum number of threads that can be used for database activity at a +// single time. +const uint32_t kMaxConnectionThreadCount = 20; + +static_assert(kMaxConnectionThreadCount, "Must have at least one thread!"); + +// The maximum number of threads to keep when idle. Threads that become idle in +// excess of this number will be shut down immediately. +const uint32_t kMaxIdleConnectionThreadCount = 2; + +static_assert(kMaxConnectionThreadCount >= kMaxIdleConnectionThreadCount, + "Idle thread limit must be less than total thread limit!"); + +// The length of time that database connections will be held open after all +// transactions have completed before doing idle maintenance. +const uint32_t kConnectionIdleMaintenanceMS = 2 * 1000; // 2 seconds + +// The length of time that database connections will be held open after all +// transactions and maintenance have completed. +const uint32_t kConnectionIdleCloseMS = 10 * 1000; // 10 seconds + +// The length of time that idle threads will stay alive before being shut down. +const uint32_t kConnectionThreadIdleMS = 30 * 1000; // 30 seconds + +#define SAVEPOINT_CLAUSE "SAVEPOINT sp;" + +const uint32_t kFileCopyBufferSize = 32768; + +#define JOURNAL_DIRECTORY_NAME "journals" + +const char kFileManagerDirectoryNameSuffix[] = ".files"; +const char kSQLiteJournalSuffix[] = ".sqlite-journal"; +const char kSQLiteSHMSuffix[] = ".sqlite-shm"; +const char kSQLiteWALSuffix[] = ".sqlite-wal"; + +const char kPrefIndexedDBEnabled[] = "dom.indexedDB.enabled"; + +const char kPrefFileHandleEnabled[] = "dom.fileHandle.enabled"; + +#define IDB_PREFIX "indexedDB" + +#define PERMISSION_STRING_CHROME_BASE IDB_PREFIX "-chrome-" +#define PERMISSION_STRING_CHROME_READ_SUFFIX "-read" +#define PERMISSION_STRING_CHROME_WRITE_SUFFIX "-write" + +#ifdef DEBUG + +const int32_t kDEBUGThreadPriority = nsISupportsPriority::PRIORITY_NORMAL; +const uint32_t kDEBUGThreadSleepMS = 0; + +const int32_t kDEBUGTransactionThreadPriority = + nsISupportsPriority::PRIORITY_NORMAL; +const uint32_t kDEBUGTransactionThreadSleepMS = 0; + +#endif + +template <size_t N> +constexpr size_t +LiteralStringLength(const char (&aArr)[N]) +{ + static_assert(N, "Zero-length string literal?!"); + + // Don't include the null terminator. + return N - 1; +} + +/******************************************************************************* + * Metadata classes + ******************************************************************************/ + +struct FullIndexMetadata +{ + IndexMetadata mCommonMetadata; + + bool mDeleted; + +public: + FullIndexMetadata() + : mCommonMetadata(0, nsString(), KeyPath(0), nsCString(), false, false, false) + , mDeleted(false) + { + // This can happen either on the QuotaManager IO thread or on a + // versionchange transaction thread. These threads can never race so this is + // totally safe. + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata) + +private: + ~FullIndexMetadata() + { } +}; + +typedef nsRefPtrHashtable<nsUint64HashKey, FullIndexMetadata> IndexTable; + +struct FullObjectStoreMetadata +{ + ObjectStoreMetadata mCommonMetadata; + IndexTable mIndexes; + + // These two members are only ever touched on a transaction thread! + int64_t mNextAutoIncrementId; + int64_t mCommittedAutoIncrementId; + + bool mDeleted; + +public: + FullObjectStoreMetadata() + : mCommonMetadata(0, nsString(), KeyPath(0), false) + , mNextAutoIncrementId(0) + , mCommittedAutoIncrementId(0) + , mDeleted(false) + { + // This can happen either on the QuotaManager IO thread or on a + // versionchange transaction thread. These threads can never race so this is + // totally safe. + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata); + + bool + HasLiveIndexes() const; + +private: + ~FullObjectStoreMetadata() + { } +}; + +typedef nsRefPtrHashtable<nsUint64HashKey, FullObjectStoreMetadata> + ObjectStoreTable; + +struct FullDatabaseMetadata +{ + DatabaseMetadata mCommonMetadata; + nsCString mDatabaseId; + nsString mFilePath; + ObjectStoreTable mObjectStores; + + int64_t mNextObjectStoreId; + int64_t mNextIndexId; + +public: + explicit FullDatabaseMetadata(const DatabaseMetadata& aCommonMetadata) + : mCommonMetadata(aCommonMetadata) + , mNextObjectStoreId(0) + , mNextIndexId(0) + { + AssertIsOnBackgroundThread(); + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullDatabaseMetadata) + + already_AddRefed<FullDatabaseMetadata> + Duplicate() const; + +private: + ~FullDatabaseMetadata() + { } +}; + +template <class MetadataType> +class MOZ_STACK_CLASS MetadataNameOrIdMatcher final +{ + typedef MetadataNameOrIdMatcher<MetadataType> SelfType; + + const int64_t mId; + const nsString mName; + RefPtr<MetadataType> mMetadata; + bool mCheckName; + +public: + template <class Enumerable> + static MetadataType* + Match(const Enumerable& aEnumerable, uint64_t aId, const nsAString& aName) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aId); + + SelfType closure(aId, aName); + MatchHelper(aEnumerable, &closure); + + return closure.mMetadata; + } + + template <class Enumerable> + static MetadataType* + Match(const Enumerable& aEnumerable, uint64_t aId) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aId); + + SelfType closure(aId); + MatchHelper(aEnumerable, &closure); + + return closure.mMetadata; + } + +private: + MetadataNameOrIdMatcher(const int64_t& aId, const nsAString& aName) + : mId(aId) + , mName(PromiseFlatString(aName)) + , mMetadata(nullptr) + , mCheckName(true) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aId); + } + + explicit MetadataNameOrIdMatcher(const int64_t& aId) + : mId(aId) + , mMetadata(nullptr) + , mCheckName(false) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aId); + } + + template <class Enumerable> + static void + MatchHelper(const Enumerable& aEnumerable, SelfType* aClosure) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aClosure); + + for (auto iter = aEnumerable.ConstIter(); !iter.Done(); iter.Next()) { +#ifdef DEBUG + const uint64_t key = iter.Key(); +#endif + MetadataType* value = iter.UserData(); + MOZ_ASSERT(key != 0); + MOZ_ASSERT(value); + + if (!value->mDeleted && + (aClosure->mId == value->mCommonMetadata.id() || + (aClosure->mCheckName && + aClosure->mName == value->mCommonMetadata.name()))) { + aClosure->mMetadata = value; + break; + } + } + } +}; + +struct IndexDataValue final +{ + int64_t mIndexId; + Key mKey; + Key mSortKey; + bool mUnique; + + IndexDataValue() + : mIndexId(0) + , mUnique(false) + { + MOZ_COUNT_CTOR(IndexDataValue); + } + + explicit + IndexDataValue(const IndexDataValue& aOther) + : mIndexId(aOther.mIndexId) + , mKey(aOther.mKey) + , mSortKey(aOther.mSortKey) + , mUnique(aOther.mUnique) + { + MOZ_ASSERT(!aOther.mKey.IsUnset()); + + MOZ_COUNT_CTOR(IndexDataValue); + } + + IndexDataValue(int64_t aIndexId, bool aUnique, const Key& aKey) + : mIndexId(aIndexId) + , mKey(aKey) + , mUnique(aUnique) + { + MOZ_ASSERT(!aKey.IsUnset()); + + MOZ_COUNT_CTOR(IndexDataValue); + } + + IndexDataValue(int64_t aIndexId, bool aUnique, const Key& aKey, + const Key& aSortKey) + : mIndexId(aIndexId) + , mKey(aKey) + , mSortKey(aSortKey) + , mUnique(aUnique) + { + MOZ_ASSERT(!aKey.IsUnset()); + + MOZ_COUNT_CTOR(IndexDataValue); + } + + ~IndexDataValue() + { + MOZ_COUNT_DTOR(IndexDataValue); + } + + bool + operator==(const IndexDataValue& aOther) const + { + if (mIndexId != aOther.mIndexId) { + return false; + } + if (mSortKey.IsUnset()) { + return mKey == aOther.mKey; + } + return mSortKey == aOther.mSortKey; + } + + bool + operator<(const IndexDataValue& aOther) const + { + if (mIndexId == aOther.mIndexId) { + if (mSortKey.IsUnset()) { + return mKey < aOther.mKey; + } + return mSortKey < aOther.mSortKey; + } + + return mIndexId < aOther.mIndexId; + } +}; + +/******************************************************************************* + * SQLite functions + ******************************************************************************/ + +int32_t +MakeSchemaVersion(uint32_t aMajorSchemaVersion, + uint32_t aMinorSchemaVersion) +{ + return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion); +} + +uint32_t +HashName(const nsAString& aName) +{ + struct Helper + { + static uint32_t + RotateBitsLeft32(uint32_t aValue, uint8_t aBits) + { + MOZ_ASSERT(aBits < 32); + return (aValue << aBits) | (aValue >> (32 - aBits)); + } + }; + + static const uint32_t kGoldenRatioU32 = 0x9e3779b9u; + + const char16_t* str = aName.BeginReading(); + size_t length = aName.Length(); + + uint32_t hash = 0; + for (size_t i = 0; i < length; i++) { + hash = kGoldenRatioU32 * (Helper::RotateBitsLeft32(hash, 5) ^ str[i]); + } + + return hash; +} + +nsresult +ClampResultCode(nsresult aResultCode) +{ + if (NS_SUCCEEDED(aResultCode) || + NS_ERROR_GET_MODULE(aResultCode) == NS_ERROR_MODULE_DOM_INDEXEDDB) { + return aResultCode; + } + + switch (aResultCode) { + case NS_ERROR_FILE_NO_DEVICE_SPACE: + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + case NS_ERROR_STORAGE_CONSTRAINT: + return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + default: +#ifdef DEBUG + nsPrintfCString message("Converting non-IndexedDB error code (0x%X) to " + "NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR", + aResultCode); + NS_WARNING(message.get()); +#else + ; +#endif + } + + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; +} + +void +GetDatabaseFilename(const nsAString& aName, + nsAutoString& aDatabaseFilename) +{ + MOZ_ASSERT(aDatabaseFilename.IsEmpty()); + + aDatabaseFilename.AppendInt(HashName(aName)); + + nsCString escapedName; + if (!NS_Escape(NS_ConvertUTF16toUTF8(aName), escapedName, url_XPAlphas)) { + MOZ_CRASH("Can't escape database name!"); + } + + const char* forwardIter = escapedName.BeginReading(); + const char* backwardIter = escapedName.EndReading() - 1; + + nsAutoCString substring; + while (forwardIter <= backwardIter && substring.Length() < 21) { + if (substring.Length() % 2) { + substring.Append(*backwardIter--); + } else { + substring.Append(*forwardIter++); + } + } + + aDatabaseFilename.AppendASCII(substring.get(), substring.Length()); +} + +uint32_t +CompressedByteCountForNumber(uint64_t aNumber) +{ + // All bytes have 7 bits available. + uint32_t count = 1; + while ((aNumber >>= 7)) { + count++; + } + + return count; +} + +uint32_t +CompressedByteCountForIndexId(int64_t aIndexId) +{ + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(UINT64_MAX - uint64_t(aIndexId) >= uint64_t(aIndexId), + "Overflow!"); + + return CompressedByteCountForNumber(uint64_t(aIndexId * 2)); +} + +void +WriteCompressedNumber(uint64_t aNumber, uint8_t** aIterator) +{ + MOZ_ASSERT(aIterator); + MOZ_ASSERT(*aIterator); + + uint8_t*& buffer = *aIterator; + +#ifdef DEBUG + const uint8_t* bufferStart = buffer; + const uint64_t originalNumber = aNumber; +#endif + + while (true) { + uint64_t shiftedNumber = aNumber >> 7; + if (shiftedNumber) { + *buffer++ = uint8_t(0x80 | (aNumber & 0x7f)); + aNumber = shiftedNumber; + } else { + *buffer++ = uint8_t(aNumber); + break; + } + } + + MOZ_ASSERT(buffer > bufferStart); + MOZ_ASSERT(uint32_t(buffer - bufferStart) == + CompressedByteCountForNumber(originalNumber)); +} + +uint64_t +ReadCompressedNumber(const uint8_t** aIterator, const uint8_t* aEnd) +{ + MOZ_ASSERT(aIterator); + MOZ_ASSERT(*aIterator); + MOZ_ASSERT(aEnd); + MOZ_ASSERT(*aIterator < aEnd); + + const uint8_t*& buffer = *aIterator; + + uint8_t shiftCounter = 0; + uint64_t result = 0; + + while (true) { + MOZ_ASSERT(shiftCounter <= 56, "Shifted too many bits!"); + + result += (uint64_t(*buffer & 0x7f) << shiftCounter); + shiftCounter += 7; + + if (!(*buffer++ & 0x80)) { + break; + } + + if (NS_WARN_IF(buffer == aEnd)) { + MOZ_ASSERT(false); + break; + } + } + + return result; +} + +void +WriteCompressedIndexId(int64_t aIndexId, bool aUnique, uint8_t** aIterator) +{ + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(UINT64_MAX - uint64_t(aIndexId) >= uint64_t(aIndexId), + "Overflow!"); + MOZ_ASSERT(aIterator); + MOZ_ASSERT(*aIterator); + + const uint64_t indexId = (uint64_t(aIndexId * 2) | (aUnique ? 1 : 0)); + WriteCompressedNumber(indexId, aIterator); +} + +void +ReadCompressedIndexId(const uint8_t** aIterator, + const uint8_t* aEnd, + int64_t* aIndexId, + bool* aUnique) +{ + MOZ_ASSERT(aIterator); + MOZ_ASSERT(*aIterator); + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(aUnique); + + uint64_t indexId = ReadCompressedNumber(aIterator, aEnd); + + if (indexId % 2) { + *aUnique = true; + indexId--; + } else { + *aUnique = false; + } + + MOZ_ASSERT(UINT64_MAX / 2 >= uint64_t(indexId), "Bad index id!"); + + *aIndexId = int64_t(indexId / 2); +} + +// static +nsresult +MakeCompressedIndexDataValues( + const FallibleTArray<IndexDataValue>& aIndexValues, + UniqueFreePtr<uint8_t>& aCompressedIndexDataValues, + uint32_t* aCompressedIndexDataValuesLength) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!aCompressedIndexDataValues); + MOZ_ASSERT(aCompressedIndexDataValuesLength); + + PROFILER_LABEL("IndexedDB", + "MakeCompressedIndexDataValues", + js::ProfileEntry::Category::STORAGE); + + const uint32_t arrayLength = aIndexValues.Length(); + if (!arrayLength) { + *aCompressedIndexDataValuesLength = 0; + return NS_OK; + } + + // First calculate the size of the final buffer. + uint32_t blobDataLength = 0; + + for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { + const IndexDataValue& info = aIndexValues[arrayIndex]; + const nsCString& keyBuffer = info.mKey.GetBuffer(); + const nsCString& sortKeyBuffer = info.mSortKey.GetBuffer(); + const uint32_t keyBufferLength = keyBuffer.Length(); + const uint32_t sortKeyBufferLength = sortKeyBuffer.Length(); + + MOZ_ASSERT(!keyBuffer.IsEmpty()); + + // Don't let |infoLength| overflow. + if (NS_WARN_IF(UINT32_MAX - keyBuffer.Length() < + CompressedByteCountForIndexId(info.mIndexId) + + CompressedByteCountForNumber(keyBufferLength) + + CompressedByteCountForNumber(sortKeyBufferLength))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const uint32_t infoLength = + CompressedByteCountForIndexId(info.mIndexId) + + CompressedByteCountForNumber(keyBufferLength) + + CompressedByteCountForNumber(sortKeyBufferLength) + + keyBufferLength + + sortKeyBufferLength; + + // Don't let |blobDataLength| overflow. + if (NS_WARN_IF(UINT32_MAX - infoLength < blobDataLength)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + blobDataLength += infoLength; + } + + UniqueFreePtr<uint8_t> blobData( + static_cast<uint8_t*>(malloc(blobDataLength))); + if (NS_WARN_IF(!blobData)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + uint8_t* blobDataIter = blobData.get(); + + for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { + const IndexDataValue& info = aIndexValues[arrayIndex]; + const nsCString& keyBuffer = info.mKey.GetBuffer(); + const nsCString& sortKeyBuffer = info.mSortKey.GetBuffer(); + const uint32_t keyBufferLength = keyBuffer.Length(); + const uint32_t sortKeyBufferLength = sortKeyBuffer.Length(); + + WriteCompressedIndexId(info.mIndexId, info.mUnique, &blobDataIter); + WriteCompressedNumber(keyBufferLength, &blobDataIter); + + memcpy(blobDataIter, keyBuffer.get(), keyBufferLength); + blobDataIter += keyBufferLength; + + WriteCompressedNumber(sortKeyBufferLength, &blobDataIter); + + memcpy(blobDataIter, sortKeyBuffer.get(), sortKeyBufferLength); + blobDataIter += sortKeyBufferLength; + } + + MOZ_ASSERT(blobDataIter == blobData.get() + blobDataLength); + + aCompressedIndexDataValues.swap(blobData); + *aCompressedIndexDataValuesLength = uint32_t(blobDataLength); + + return NS_OK; +} + +nsresult +ReadCompressedIndexDataValuesFromBlob(const uint8_t* aBlobData, + uint32_t aBlobDataLength, + nsTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aBlobData); + MOZ_ASSERT(aBlobDataLength); + MOZ_ASSERT(aIndexValues.IsEmpty()); + + PROFILER_LABEL("IndexedDB", + "ReadCompressedIndexDataValuesFromBlob", + js::ProfileEntry::Category::STORAGE); + + const uint8_t* blobDataIter = aBlobData; + const uint8_t* blobDataEnd = aBlobData + aBlobDataLength; + + while (blobDataIter < blobDataEnd) { + int64_t indexId; + bool unique; + ReadCompressedIndexId(&blobDataIter, blobDataEnd, &indexId, &unique); + + if (NS_WARN_IF(blobDataIter == blobDataEnd)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + // Read key buffer length. + const uint64_t keyBufferLength = + ReadCompressedNumber(&blobDataIter, blobDataEnd); + + if (NS_WARN_IF(blobDataIter == blobDataEnd) || + NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) || + NS_WARN_IF(blobDataIter + keyBufferLength > blobDataEnd)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + nsCString keyBuffer(reinterpret_cast<const char*>(blobDataIter), + uint32_t(keyBufferLength)); + blobDataIter += keyBufferLength; + + IndexDataValue idv(indexId, unique, Key(keyBuffer)); + + // Read sort key buffer length. + const uint64_t sortKeyBufferLength = + ReadCompressedNumber(&blobDataIter, blobDataEnd); + + if (sortKeyBufferLength > 0) { + if (NS_WARN_IF(blobDataIter == blobDataEnd) || + NS_WARN_IF(sortKeyBufferLength > uint64_t(UINT32_MAX)) || + NS_WARN_IF(blobDataIter + sortKeyBufferLength > blobDataEnd)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + nsCString sortKeyBuffer(reinterpret_cast<const char*>(blobDataIter), + uint32_t(sortKeyBufferLength)); + blobDataIter += sortKeyBufferLength; + + idv.mSortKey = Key(sortKeyBuffer); + } + + if (NS_WARN_IF(!aIndexValues.InsertElementSorted(idv, fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + } + + MOZ_ASSERT(blobDataIter == blobDataEnd); + + return NS_OK; +} + +// static +template <typename T> +nsresult +ReadCompressedIndexDataValuesFromSource(T* aSource, + uint32_t aColumnIndex, + nsTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aSource); + MOZ_ASSERT(aIndexValues.IsEmpty()); + + int32_t columnType; + nsresult rv = aSource->GetTypeOfIndex(aColumnIndex, &columnType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) { + return NS_OK; + } + + MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_BLOB); + + const uint8_t* blobData; + uint32_t blobDataLength; + rv = aSource->GetSharedBlob(aColumnIndex, &blobDataLength, &blobData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!blobDataLength)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + rv = ReadCompressedIndexDataValuesFromBlob(blobData, + blobDataLength, + aIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +ReadCompressedIndexDataValues(mozIStorageStatement* aStatement, + uint32_t aColumnIndex, + nsTArray<IndexDataValue>& aIndexValues) +{ + return ReadCompressedIndexDataValuesFromSource(aStatement, + aColumnIndex, + aIndexValues); +} + +nsresult +ReadCompressedIndexDataValues(mozIStorageValueArray* aValues, + uint32_t aColumnIndex, + nsTArray<IndexDataValue>& aIndexValues) +{ + return ReadCompressedIndexDataValuesFromSource(aValues, + aColumnIndex, + aIndexValues); +} + +nsresult +CreateFileTables(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "CreateFileTables", + js::ProfileEntry::Category::STORAGE); + + // Table `file` + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE file (" + "id INTEGER PRIMARY KEY, " + "refcount INTEGER NOT NULL" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_insert_trigger " + "AFTER INSERT ON object_data " + "FOR EACH ROW " + "WHEN NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(NULL, NEW.file_ids); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_update_trigger " + "AFTER UPDATE OF file_ids ON object_data " + "FOR EACH ROW " + "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_delete_trigger " + "AFTER DELETE ON object_data " + "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NULL); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER file_update_trigger " + "AFTER UPDATE ON file " + "FOR EACH ROW WHEN NEW.refcount = 0 " + "BEGIN " + "DELETE FROM file WHERE id = OLD.id; " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +CreateTables(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "CreateTables", + js::ProfileEntry::Category::STORAGE); + + // Table `database` + + // There are two reasons for having the origin column. + // First, we can ensure that we don't have collisions in the origin hash we + // use for the path because when we open the db we can make sure that the + // origins exactly match. Second, chrome code crawling through the idb + // directory can figure out the origin of every db without having to + // reverse-engineer our hash scheme. + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE database" + "( name TEXT PRIMARY KEY" + ", origin TEXT NOT NULL" + ", version INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_time INTEGER NOT NULL DEFAULT 0" + ", last_analyze_time INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_size INTEGER NOT NULL DEFAULT 0" + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Table `object_store` + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_store" + "( id INTEGER PRIMARY KEY" + ", auto_increment INTEGER NOT NULL DEFAULT 0" + ", name TEXT NOT NULL" + ", key_path TEXT" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Table `object_store_index` + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_store_index" + "( id INTEGER PRIMARY KEY" + ", object_store_id INTEGER NOT NULL" + ", name TEXT NOT NULL" + ", key_path TEXT NOT NULL" + ", unique_index INTEGER NOT NULL" + ", multientry INTEGER NOT NULL" + ", locale TEXT" + ", is_auto_locale BOOLEAN NOT NULL" + ", FOREIGN KEY (object_store_id) " + "REFERENCES object_store(id) " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Table `object_data` + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_data" + "( object_store_id INTEGER NOT NULL" + ", key BLOB NOT NULL" + ", index_data_values BLOB DEFAULT NULL" + ", file_ids TEXT" + ", data BLOB NOT NULL" + ", PRIMARY KEY (object_store_id, key)" + ", FOREIGN KEY (object_store_id) " + "REFERENCES object_store(id) " + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Table `index_data` + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE index_data" + "( index_id INTEGER NOT NULL" + ", value BLOB NOT NULL" + ", object_data_key BLOB NOT NULL" + ", object_store_id INTEGER NOT NULL" + ", value_locale BLOB" + ", PRIMARY KEY (index_id, value, object_data_key)" + ", FOREIGN KEY (index_id) " + "REFERENCES object_store_index(id) " + ", FOREIGN KEY (object_store_id, object_data_key) " + "REFERENCES object_data(object_store_id, key) " + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX index_data_value_locale_index " + "ON index_data (index_id, value_locale, object_data_key, value) " + "WHERE value_locale IS NOT NULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Table `unique_index_data` + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE unique_index_data" + "( index_id INTEGER NOT NULL" + ", value BLOB NOT NULL" + ", object_store_id INTEGER NOT NULL" + ", object_data_key BLOB NOT NULL" + ", value_locale BLOB" + ", PRIMARY KEY (index_id, value)" + ", FOREIGN KEY (index_id) " + "REFERENCES object_store_index(id) " + ", FOREIGN KEY (object_store_id, object_data_key) " + "REFERENCES object_data(object_store_id, key) " + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX unique_index_data_value_locale_index " + "ON unique_index_data (index_id, value_locale, object_data_key, value) " + "WHERE value_locale IS NOT NULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = CreateFileTables(aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(kSQLiteSchemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom4To5(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom4To5", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + + // All we changed is the type of the version column, so lets try to + // convert that to an integer, and if we fail, set it to 0. + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT name, version, dataVersion " + "FROM database" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString name; + int32_t intVersion; + int64_t dataVersion; + + { + mozStorageStatementScoper scoper(stmt); + + bool hasResults; + rv = stmt->ExecuteStep(&hasResults); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (NS_WARN_IF(!hasResults)) { + return NS_ERROR_FAILURE; + } + + nsString version; + rv = stmt->GetString(1, version); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + intVersion = version.ToInteger(&rv); + if (NS_FAILED(rv)) { + intVersion = 0; + } + + rv = stmt->GetString(0, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->GetInt64(2, &dataVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE database" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE database (" + "name TEXT NOT NULL, " + "version INTEGER NOT NULL DEFAULT 0, " + "dataVersion INTEGER NOT NULL" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO database (name, version, dataVersion) " + "VALUES (:name, :version, :dataVersion)" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + { + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindStringParameter(0, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32Parameter(1, intVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64Parameter(2, dataVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = aConnection->SetSchemaVersion(5); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom5To6(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom5To6", + js::ProfileEntry::Category::STORAGE); + + // First, drop all the indexes we're no longer going to use. + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX key_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX ai_key_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX value_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX ai_value_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now, reorder the columns of object_data to put the blob data last. We do + // this by copying into a temporary table, dropping the original, then copying + // back into a newly created table. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "key_value, " + "data " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, key_value, data " + "FROM object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_data (" + "id INTEGER PRIMARY KEY, " + "object_store_id INTEGER NOT NULL, " + "key_value DEFAULT NULL, " + "data BLOB NOT NULL, " + "UNIQUE (object_store_id, key_value), " + "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_data " + "SELECT id, object_store_id, key_value, data " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We need to add a unique constraint to our ai_object_data table. Copy all + // the data out of it using a temporary table as before. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "data " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, data " + "FROM ai_object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE ai_object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE ai_object_data (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "object_store_id INTEGER NOT NULL, " + "data BLOB NOT NULL, " + "UNIQUE (object_store_id, id), " + "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO ai_object_data " + "SELECT id, object_store_id, data " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Fix up the index_data table. We're reordering the columns as well as + // changing the primary key from being a simple id to being a composite. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE index_data (" + "index_id INTEGER NOT NULL, " + "value NOT NULL, " + "object_data_key NOT NULL, " + "object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, object_data_key), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE, " + "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT OR IGNORE INTO index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX index_data_object_data_id_index " + "ON index_data (object_data_id);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Fix up the unique_index_data table. We're reordering the columns as well as + // changing the primary key from being a simple id to being a composite. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE unique_index_data (" + "index_id INTEGER NOT NULL, " + "value NOT NULL, " + "object_data_key NOT NULL, " + "object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, object_data_key), " + "UNIQUE (index_id, value), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE " + "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO unique_index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX unique_index_data_object_data_id_index " + "ON unique_index_data (object_data_id);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Fix up the ai_index_data table. We're reordering the columns as well as + // changing the primary key from being a simple id to being a composite. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "ai_object_data_id " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, ai_object_data_id " + "FROM ai_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE ai_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE ai_index_data (" + "index_id INTEGER NOT NULL, " + "value NOT NULL, " + "ai_object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, ai_object_data_id), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE, " + "FOREIGN KEY (ai_object_data_id) REFERENCES ai_object_data(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT OR IGNORE INTO ai_index_data " + "SELECT index_id, value, ai_object_data_id " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX ai_index_data_ai_object_data_id_index " + "ON ai_index_data (ai_object_data_id);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Fix up the ai_unique_index_data table. We're reordering the columns as well + // as changing the primary key from being a simple id to being a composite. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "ai_object_data_id " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, ai_object_data_id " + "FROM ai_unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE ai_unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE ai_unique_index_data (" + "index_id INTEGER NOT NULL, " + "value NOT NULL, " + "ai_object_data_id INTEGER NOT NULL, " + "UNIQUE (index_id, value), " + "PRIMARY KEY (index_id, value, ai_object_data_id), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE, " + "FOREIGN KEY (ai_object_data_id) REFERENCES ai_object_data(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO ai_unique_index_data " + "SELECT index_id, value, ai_object_data_id " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX ai_unique_index_data_ai_object_data_id_index " + "ON ai_unique_index_data (ai_object_data_id);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(6); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom6To7(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom6To7", + js::ProfileEntry::Category::STORAGE); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id, " + "name, " + "key_path, " + "auto_increment" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, name, key_path, auto_increment " + "FROM object_store;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_store;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_store (" + "id INTEGER PRIMARY KEY, " + "auto_increment INTEGER NOT NULL DEFAULT 0, " + "name TEXT NOT NULL, " + "key_path TEXT, " + "UNIQUE (name)" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_store " + "SELECT id, auto_increment, name, nullif(key_path, '') " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(7); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom7To8(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom7To8", + js::ProfileEntry::Category::STORAGE); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id, " + "object_store_id, " + "name, " + "key_path, " + "unique_index, " + "object_store_autoincrement" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, name, key_path, " + "unique_index, object_store_autoincrement " + "FROM object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_store_index (" + "id INTEGER, " + "object_store_id INTEGER NOT NULL, " + "name TEXT NOT NULL, " + "key_path TEXT NOT NULL, " + "unique_index INTEGER NOT NULL, " + "multientry INTEGER NOT NULL, " + "object_store_autoincrement INTERGER NOT NULL, " + "PRIMARY KEY (id), " + "UNIQUE (object_store_id, name), " + "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_store_index " + "SELECT id, object_store_id, name, key_path, " + "unique_index, 0, object_store_autoincrement " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(8); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +class CompressDataBlobsFunction final + : public mozIStorageFunction +{ +public: + NS_DECL_ISUPPORTS + +private: + ~CompressDataBlobsFunction() + { } + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override + { + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + PROFILER_LABEL("IndexedDB", + "CompressDataBlobsFunction::OnFunctionCall", + js::ProfileEntry::Category::STORAGE); + + uint32_t argc; + nsresult rv = aArguments->GetNumEntries(&argc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (argc != 1) { + NS_WARNING("Don't call me with the wrong number of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + int32_t type; + rv = aArguments->GetTypeOfIndex(0, &type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (type != mozIStorageStatement::VALUE_TYPE_BLOB) { + NS_WARNING("Don't call me with the wrong type of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + const uint8_t* uncompressed; + uint32_t uncompressedLength; + rv = aArguments->GetSharedBlob(0, &uncompressedLength, &uncompressed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength); + UniqueFreePtr<uint8_t> compressed( + static_cast<uint8_t*>(malloc(compressedLength))); + if (NS_WARN_IF(!compressed)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + snappy::RawCompress(reinterpret_cast<const char*>(uncompressed), + uncompressedLength, + reinterpret_cast<char*>(compressed.get()), + &compressedLength); + + std::pair<uint8_t *, int> data(compressed.release(), + int(compressedLength)); + + nsCOMPtr<nsIVariant> result = new mozilla::storage::AdoptedBlobVariant(data); + + result.forget(aResult); + return NS_OK; + } +}; + +nsresult +UpgradeSchemaFrom8To9_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom8To9_0", + js::ProfileEntry::Category::STORAGE); + + // We no longer use the dataVersion column. + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE database SET dataVersion = 0;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageFunction> compressor = new CompressDataBlobsFunction(); + + NS_NAMED_LITERAL_CSTRING(compressorName, "compress"); + + rv = aConnection->CreateFunction(compressorName, 1, compressor); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Turn off foreign key constraints before we do anything here. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE object_data SET data = compress(data);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE ai_object_data SET data = compress(data);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->RemoveFunction(compressorName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(9, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom9_0To10_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom9_0To10_0", + js::ProfileEntry::Category::STORAGE); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE object_data ADD COLUMN file_ids TEXT;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE ai_object_data ADD COLUMN file_ids TEXT;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = CreateFileTables(aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(10, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom10_0To11_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom10_0To11_0", + js::ProfileEntry::Category::STORAGE); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id, " + "object_store_id, " + "name, " + "key_path, " + "unique_index, " + "multientry" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, name, key_path, " + "unique_index, multientry " + "FROM object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_store_index (" + "id INTEGER PRIMARY KEY, " + "object_store_id INTEGER NOT NULL, " + "name TEXT NOT NULL, " + "key_path TEXT NOT NULL, " + "unique_index INTEGER NOT NULL, " + "multientry INTEGER NOT NULL, " + "UNIQUE (object_store_id, name), " + "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_store_index " + "SELECT id, object_store_id, name, key_path, " + "unique_index, multientry " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TRIGGER object_data_insert_trigger;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_data (object_store_id, key_value, data, file_ids) " + "SELECT object_store_id, id, data, file_ids " + "FROM ai_object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_insert_trigger " + "AFTER INSERT ON object_data " + "FOR EACH ROW " + "WHEN NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(NULL, NEW.file_ids); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO index_data (index_id, value, object_data_key, object_data_id) " + "SELECT ai_index_data.index_id, ai_index_data.value, ai_index_data.ai_object_data_id, object_data.id " + "FROM ai_index_data " + "INNER JOIN object_store_index ON " + "object_store_index.id = ai_index_data.index_id " + "INNER JOIN object_data ON " + "object_data.object_store_id = object_store_index.object_store_id AND " + "object_data.key_value = ai_index_data.ai_object_data_id;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO unique_index_data (index_id, value, object_data_key, object_data_id) " + "SELECT ai_unique_index_data.index_id, ai_unique_index_data.value, ai_unique_index_data.ai_object_data_id, object_data.id " + "FROM ai_unique_index_data " + "INNER JOIN object_store_index ON " + "object_store_index.id = ai_unique_index_data.index_id " + "INNER JOIN object_data ON " + "object_data.object_store_id = object_store_index.object_store_id AND " + "object_data.key_value = ai_unique_index_data.ai_object_data_id;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE object_store " + "SET auto_increment = (SELECT max(id) FROM ai_object_data) + 1 " + "WHERE auto_increment;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE ai_unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE ai_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE ai_object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(11, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +class EncodeKeysFunction final + : public mozIStorageFunction +{ +public: + NS_DECL_ISUPPORTS + +private: + ~EncodeKeysFunction() + { } + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override + { + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + PROFILER_LABEL("IndexedDB", + "EncodeKeysFunction::OnFunctionCall", + js::ProfileEntry::Category::STORAGE); + + uint32_t argc; + nsresult rv = aArguments->GetNumEntries(&argc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (argc != 1) { + NS_WARNING("Don't call me with the wrong number of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + int32_t type; + rv = aArguments->GetTypeOfIndex(0, &type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Key key; + if (type == mozIStorageStatement::VALUE_TYPE_INTEGER) { + int64_t intKey; + aArguments->GetInt64(0, &intKey); + key.SetFromInteger(intKey); + } else if (type == mozIStorageStatement::VALUE_TYPE_TEXT) { + nsString stringKey; + aArguments->GetString(0, stringKey); + key.SetFromString(stringKey); + } else { + NS_WARNING("Don't call me with the wrong type of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + const nsCString& buffer = key.GetBuffer(); + + std::pair<const void *, int> data(static_cast<const void*>(buffer.get()), + int(buffer.Length())); + + nsCOMPtr<nsIVariant> result = new mozilla::storage::BlobVariant(data); + + result.forget(aResult); + return NS_OK; + } +}; + +nsresult +UpgradeSchemaFrom11_0To12_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom11_0To12_0", + js::ProfileEntry::Category::STORAGE); + + NS_NAMED_LITERAL_CSTRING(encoderName, "encode"); + + nsCOMPtr<mozIStorageFunction> encoder = new EncodeKeysFunction(); + + nsresult rv = aConnection->CreateFunction(encoderName, 1, encoder); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "key_value, " + "data, " + "file_ids " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, encode(key_value), data, file_ids " + "FROM object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_data (" + "id INTEGER PRIMARY KEY, " + "object_store_id INTEGER NOT NULL, " + "key_value BLOB DEFAULT NULL, " + "file_ids TEXT, " + "data BLOB NOT NULL, " + "UNIQUE (object_store_id, key_value), " + "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_data " + "SELECT id, object_store_id, key_value, file_ids, data " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_insert_trigger " + "AFTER INSERT ON object_data " + "FOR EACH ROW " + "WHEN NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(NULL, NEW.file_ids); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_update_trigger " + "AFTER UPDATE OF file_ids ON object_data " + "FOR EACH ROW " + "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_delete_trigger " + "AFTER DELETE ON object_data " + "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NULL); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, encode(value), encode(object_data_key), object_data_id " + "FROM index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE index_data (" + "index_id INTEGER NOT NULL, " + "value BLOB NOT NULL, " + "object_data_key BLOB NOT NULL, " + "object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, object_data_key), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE, " + "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX index_data_object_data_id_index " + "ON index_data (object_data_id);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, encode(value), encode(object_data_key), object_data_id " + "FROM unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE unique_index_data (" + "index_id INTEGER NOT NULL, " + "value BLOB NOT NULL, " + "object_data_key BLOB NOT NULL, " + "object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, object_data_key), " + "UNIQUE (index_id, value), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE " + "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " + "CASCADE" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO unique_index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX unique_index_data_object_data_id_index " + "ON unique_index_data (object_data_id);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->RemoveFunction(encoderName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(12, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom12_0To13_0(mozIStorageConnection* aConnection, + bool* aVacuumNeeded) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom12_0To13_0", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + +#ifdef IDB_MOBILE + int32_t defaultPageSize; + rv = aConnection->GetDefaultPageSize(&defaultPageSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Enable auto_vacuum mode and update the page size to the platform default. + nsAutoCString upgradeQuery("PRAGMA auto_vacuum = FULL; PRAGMA page_size = "); + upgradeQuery.AppendInt(defaultPageSize); + + rv = aConnection->ExecuteSimpleSQL(upgradeQuery); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aVacuumNeeded = true; +#endif + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(13, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom13_0To14_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The only change between 13 and 14 was a different structured + // clone format, but it's backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(14, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom14_0To15_0(mozIStorageConnection* aConnection) +{ + // The only change between 14 and 15 was a different structured + // clone format, but it's backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(15, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom15_0To16_0(mozIStorageConnection* aConnection) +{ + // The only change between 15 and 16 was a different structured + // clone format, but it's backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(16, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom16_0To17_0(mozIStorageConnection* aConnection) +{ + // The only change between 16 and 17 was a different structured + // clone format, but it's backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(17, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +class UpgradeSchemaFrom17_0To18_0Helper final +{ + class InsertIndexDataValuesFunction; + class UpgradeKeyFunction; + +public: + static nsresult + DoUpgrade(mozIStorageConnection* aConnection, const nsACString& aOrigin); + +private: + static nsresult + DoUpgradeInternal(mozIStorageConnection* aConnection, + const nsACString& aOrigin); + + UpgradeSchemaFrom17_0To18_0Helper() + { + MOZ_ASSERT_UNREACHABLE("Don't create instances of this class!"); + } + + ~UpgradeSchemaFrom17_0To18_0Helper() + { + MOZ_ASSERT_UNREACHABLE("Don't create instances of this class!"); + } +}; + +class UpgradeSchemaFrom17_0To18_0Helper::InsertIndexDataValuesFunction final + : public mozIStorageFunction +{ +public: + InsertIndexDataValuesFunction() + { } + + NS_DECL_ISUPPORTS + +private: + ~InsertIndexDataValuesFunction() + { } + + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(UpgradeSchemaFrom17_0To18_0Helper:: + InsertIndexDataValuesFunction, + mozIStorageFunction); + +NS_IMETHODIMP +UpgradeSchemaFrom17_0To18_0Helper:: +InsertIndexDataValuesFunction::OnFunctionCall(mozIStorageValueArray* aValues, + nsIVariant** _retval) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aValues); + MOZ_ASSERT(_retval); + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 4); + + int32_t valueType; + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || + valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(1, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER); + + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(2, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER); + + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(3, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + } +#endif + + // Read out the previous value. It may be NULL, in which case we'll just end + // up with an empty array. + AutoTArray<IndexDataValue, 32> indexValues; + nsresult rv = ReadCompressedIndexDataValues(aValues, 0, indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t indexId; + rv = aValues->GetInt64(1, &indexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t unique; + rv = aValues->GetInt32(2, &unique); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Key value; + rv = value.SetFromValueArray(aValues, 3); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the array with the new addition. + if (NS_WARN_IF(!indexValues.SetCapacity(indexValues.Length() + 1, + fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ALWAYS_TRUE( + indexValues.InsertElementSorted(IndexDataValue(indexId, !!unique, value), + fallible)); + + // Compress the array. + UniqueFreePtr<uint8_t> indexValuesBlob; + uint32_t indexValuesBlobLength; + rv = MakeCompressedIndexDataValues(indexValues, + indexValuesBlob, + &indexValuesBlobLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The compressed blob is the result of this function. + std::pair<uint8_t *, int> indexValuesBlobPair(indexValuesBlob.release(), + indexValuesBlobLength); + + nsCOMPtr<nsIVariant> result = + new storage::AdoptedBlobVariant(indexValuesBlobPair); + + result.forget(_retval); + return NS_OK; +} + +class UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction final + : public mozIStorageFunction +{ +public: + UpgradeKeyFunction() + { } + + static nsresult + CopyAndUpgradeKeyBuffer(const uint8_t* aSource, + const uint8_t* aSourceEnd, + uint8_t* aDestination) + { + return CopyAndUpgradeKeyBufferInternal(aSource, + aSourceEnd, + aDestination, + 0 /* aTagOffset */, + 0 /* aRecursionDepth */); + } + + NS_DECL_ISUPPORTS + +private: + ~UpgradeKeyFunction() + { } + + static nsresult + CopyAndUpgradeKeyBufferInternal(const uint8_t*& aSource, + const uint8_t* aSourceEnd, + uint8_t*& aDestination, + uint8_t aTagOffset, + uint8_t aRecursionDepth); + + static uint32_t + AdjustedSize(uint32_t aMaxSize, + const uint8_t* aSource, + const uint8_t* aSourceEnd) + { + MOZ_ASSERT(aMaxSize); + MOZ_ASSERT(aSource); + MOZ_ASSERT(aSourceEnd); + MOZ_ASSERT(aSource <= aSourceEnd); + + return std::min(aMaxSize, uint32_t(aSourceEnd - aSource)); + } + + NS_DECL_MOZISTORAGEFUNCTION +}; + +// static +nsresult +UpgradeSchemaFrom17_0To18_0Helper:: +UpgradeKeyFunction::CopyAndUpgradeKeyBufferInternal(const uint8_t*& aSource, + const uint8_t* aSourceEnd, + uint8_t*& aDestination, + uint8_t aTagOffset, + uint8_t aRecursionDepth) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aSource); + MOZ_ASSERT(*aSource); + MOZ_ASSERT(aSourceEnd); + MOZ_ASSERT(aSource < aSourceEnd); + MOZ_ASSERT(aDestination); + MOZ_ASSERT(aTagOffset <= Key::kMaxArrayCollapse); + + static constexpr uint8_t kOldNumberTag = 0x1; + static constexpr uint8_t kOldDateTag = 0x2; + static constexpr uint8_t kOldStringTag = 0x3; + static constexpr uint8_t kOldArrayTag = 0x4; + static constexpr uint8_t kOldMaxType = kOldArrayTag; + + if (NS_WARN_IF(aRecursionDepth > Key::kMaxRecursionDepth)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + const uint8_t sourceTag = *aSource - (aTagOffset * kOldMaxType); + MOZ_ASSERT(sourceTag); + + if (NS_WARN_IF(sourceTag > kOldMaxType * Key::kMaxArrayCollapse)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + if (sourceTag == kOldNumberTag || sourceTag == kOldDateTag) { + // Write the new tag. + *aDestination++ = + (sourceTag == kOldNumberTag ? Key::eFloat : Key::eDate) + + (aTagOffset * Key::eMaxType); + aSource++; + + // Numbers and Dates are encoded as 64-bit integers, but trailing 0 + // bytes have been removed. + const uint32_t byteCount = + AdjustedSize(sizeof(uint64_t), aSource, aSourceEnd); + + for (uint32_t count = 0; count < byteCount; count++) { + *aDestination++ = *aSource++; + } + + return NS_OK; + } + + if (sourceTag == kOldStringTag) { + // Write the new tag. + *aDestination++ = Key::eString + (aTagOffset * Key::eMaxType); + aSource++; + + while (aSource < aSourceEnd) { + const uint8_t byte = *aSource++; + *aDestination++ = byte; + + if (!byte) { + // Just copied the terminator. + break; + } + + // Maybe copy one or two extra bytes if the byte is tagged and we have + // enough source space. + if (byte & 0x80) { + const uint32_t byteCount = + AdjustedSize((byte & 0x40) ? 2 : 1, aSource, aSourceEnd); + + for (uint32_t count = 0; count < byteCount; count++) { + *aDestination++ = *aSource++; + } + } + } + + return NS_OK; + } + + if (NS_WARN_IF(sourceTag < kOldArrayTag)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + aTagOffset++; + + if (aTagOffset == Key::kMaxArrayCollapse) { + MOZ_ASSERT(sourceTag == kOldArrayTag); + + *aDestination++ = (aTagOffset * Key::eMaxType); + aSource++; + + aTagOffset = 0; + } + + while (aSource < aSourceEnd && + (*aSource - (aTagOffset * kOldMaxType)) != Key::eTerminator) { + nsresult rv = CopyAndUpgradeKeyBufferInternal(aSource, + aSourceEnd, + aDestination, + aTagOffset, + aRecursionDepth + 1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aTagOffset = 0; + } + + if (aSource < aSourceEnd) { + MOZ_ASSERT((*aSource - (aTagOffset * kOldMaxType)) == Key::eTerminator); + *aDestination++ = Key::eTerminator + (aTagOffset * Key::eMaxType); + aSource++; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction, + mozIStorageFunction); + +NS_IMETHODIMP +UpgradeSchemaFrom17_0To18_0Helper:: +UpgradeKeyFunction::OnFunctionCall(mozIStorageValueArray* aValues, + nsIVariant** _retval) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aValues); + MOZ_ASSERT(_retval); + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 1); + + int32_t valueType; + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + } +#endif + + // Dig the old key out of the values. + const uint8_t* blobData; + uint32_t blobDataLength; + nsresult rv = aValues->GetSharedBlob(0, &blobDataLength, &blobData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Upgrading the key doesn't change the amount of space needed to hold it. + UniqueFreePtr<uint8_t> upgradedBlobData( + static_cast<uint8_t*>(malloc(blobDataLength))); + if (NS_WARN_IF(!upgradedBlobData)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = CopyAndUpgradeKeyBuffer(blobData, + blobData + blobDataLength, + upgradedBlobData.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The upgraded key is the result of this function. + std::pair<uint8_t*, int> data(upgradedBlobData.release(), + int(blobDataLength)); + + nsCOMPtr<nsIVariant> result = new mozilla::storage::AdoptedBlobVariant(data); + + result.forget(_retval); + return NS_OK; +} + +// static +nsresult +UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(mozIStorageConnection* aConnection, + const nsACString& aOrigin) +{ + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!aOrigin.IsEmpty()); + + // Register the |upgrade_key| function. + RefPtr<UpgradeKeyFunction> updateFunction = new UpgradeKeyFunction(); + + NS_NAMED_LITERAL_CSTRING(upgradeKeyFunctionName, "upgrade_key"); + + nsresult rv = + aConnection->CreateFunction(upgradeKeyFunctionName, 1, updateFunction); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Register the |insert_idv| function. + RefPtr<InsertIndexDataValuesFunction> insertIDVFunction = + new InsertIndexDataValuesFunction(); + + NS_NAMED_LITERAL_CSTRING(insertIDVFunctionName, "insert_idv"); + + rv = aConnection->CreateFunction(insertIDVFunctionName, 4, insertIDVFunction); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ALWAYS_SUCCEEDS(aConnection->RemoveFunction(upgradeKeyFunctionName)); + return rv; + } + + rv = DoUpgradeInternal(aConnection, aOrigin); + + MOZ_ALWAYS_SUCCEEDS(aConnection->RemoveFunction(upgradeKeyFunctionName)); + MOZ_ALWAYS_SUCCEEDS(aConnection->RemoveFunction(insertIDVFunctionName)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +nsresult +UpgradeSchemaFrom17_0To18_0Helper::DoUpgradeInternal( + mozIStorageConnection* aConnection, + const nsACString& aOrigin) +{ + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!aOrigin.IsEmpty()); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Drop these triggers to avoid unnecessary work during the upgrade process. + "DROP TRIGGER object_data_insert_trigger;" + "DROP TRIGGER object_data_update_trigger;" + "DROP TRIGGER object_data_delete_trigger;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Drop these indexes before we do anything else to free disk space. + "DROP INDEX index_data_object_data_id_index;" + "DROP INDEX unique_index_data_object_data_id_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Create the new tables and triggers first. + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // This will eventually become the |database| table. + "CREATE TABLE database_upgrade " + "( name TEXT PRIMARY KEY" + ", origin TEXT NOT NULL" + ", version INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_time INTEGER NOT NULL DEFAULT 0" + ", last_analyze_time INTEGER NOT NULL DEFAULT 0" + ", last_vacuum_size INTEGER NOT NULL DEFAULT 0" + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // This will eventually become the |object_store| table. + "CREATE TABLE object_store_upgrade" + "( id INTEGER PRIMARY KEY" + ", auto_increment INTEGER NOT NULL DEFAULT 0" + ", name TEXT NOT NULL" + ", key_path TEXT" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // This will eventually become the |object_store_index| table. + "CREATE TABLE object_store_index_upgrade" + "( id INTEGER PRIMARY KEY" + ", object_store_id INTEGER NOT NULL" + ", name TEXT NOT NULL" + ", key_path TEXT NOT NULL" + ", unique_index INTEGER NOT NULL" + ", multientry INTEGER NOT NULL" + ", FOREIGN KEY (object_store_id) " + "REFERENCES object_store(id) " + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // This will eventually become the |object_data| table. + "CREATE TABLE object_data_upgrade" + "( object_store_id INTEGER NOT NULL" + ", key BLOB NOT NULL" + ", index_data_values BLOB DEFAULT NULL" + ", file_ids TEXT" + ", data BLOB NOT NULL" + ", PRIMARY KEY (object_store_id, key)" + ", FOREIGN KEY (object_store_id) " + "REFERENCES object_store(id) " + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // This will eventually become the |index_data| table. + "CREATE TABLE index_data_upgrade" + "( index_id INTEGER NOT NULL" + ", value BLOB NOT NULL" + ", object_data_key BLOB NOT NULL" + ", object_store_id INTEGER NOT NULL" + ", PRIMARY KEY (index_id, value, object_data_key)" + ", FOREIGN KEY (index_id) " + "REFERENCES object_store_index(id) " + ", FOREIGN KEY (object_store_id, object_data_key) " + "REFERENCES object_data(object_store_id, key) " + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // This will eventually become the |unique_index_data| table. + "CREATE TABLE unique_index_data_upgrade" + "( index_id INTEGER NOT NULL" + ", value BLOB NOT NULL" + ", object_store_id INTEGER NOT NULL" + ", object_data_key BLOB NOT NULL" + ", PRIMARY KEY (index_id, value)" + ", FOREIGN KEY (index_id) " + "REFERENCES object_store_index(id) " + ", FOREIGN KEY (object_store_id, object_data_key) " + "REFERENCES object_data(object_store_id, key) " + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Temporarily store |index_data_values| that we build during the upgrade of + // the index tables. We will later move this to the |object_data| table. + "CREATE TEMPORARY TABLE temp_index_data_values " + "( object_store_id INTEGER NOT NULL" + ", key BLOB NOT NULL" + ", index_data_values BLOB DEFAULT NULL" + ", PRIMARY KEY (object_store_id, key)" + ") WITHOUT ROWID;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // These two triggers help build the |index_data_values| blobs. The nested + // SELECT statements help us achieve an "INSERT OR UPDATE"-like behavior. + "CREATE TEMPORARY TRIGGER unique_index_data_upgrade_insert_trigger " + "AFTER INSERT ON unique_index_data_upgrade " + "BEGIN " + "INSERT OR REPLACE INTO temp_index_data_values " + "VALUES " + "( NEW.object_store_id" + ", NEW.object_data_key" + ", insert_idv(" + "( SELECT index_data_values " + "FROM temp_index_data_values " + "WHERE object_store_id = NEW.object_store_id " + "AND key = NEW.object_data_key " + "), NEW.index_id" + ", 1" /* unique */ + ", NEW.value" + ")" + ");" + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TRIGGER index_data_upgrade_insert_trigger " + "AFTER INSERT ON index_data_upgrade " + "BEGIN " + "INSERT OR REPLACE INTO temp_index_data_values " + "VALUES " + "( NEW.object_store_id" + ", NEW.object_data_key" + ", insert_idv(" + "(" + "SELECT index_data_values " + "FROM temp_index_data_values " + "WHERE object_store_id = NEW.object_store_id " + "AND key = NEW.object_data_key " + "), NEW.index_id" + ", 0" /* not unique */ + ", NEW.value" + ")" + ");" + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |unique_index_data| table to change the column order, remove the + // ON DELETE CASCADE clauses, and to apply the WITHOUT ROWID optimization. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Insert all the data. + "INSERT INTO unique_index_data_upgrade " + "SELECT " + "unique_index_data.index_id, " + "upgrade_key(unique_index_data.value), " + "object_data.object_store_id, " + "upgrade_key(unique_index_data.object_data_key) " + "FROM unique_index_data " + "JOIN object_data " + "ON unique_index_data.object_data_id = object_data.id;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // The trigger is no longer needed. + "DROP TRIGGER unique_index_data_upgrade_insert_trigger;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // The old table is no longer needed. + "DROP TABLE unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Rename the table. + "ALTER TABLE unique_index_data_upgrade " + "RENAME TO unique_index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |index_data| table to change the column order, remove the ON + // DELETE CASCADE clauses, and to apply the WITHOUT ROWID optimization. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Insert all the data. + "INSERT INTO index_data_upgrade " + "SELECT " + "index_data.index_id, " + "upgrade_key(index_data.value), " + "upgrade_key(index_data.object_data_key), " + "object_data.object_store_id " + "FROM index_data " + "JOIN object_data " + "ON index_data.object_data_id = object_data.id;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // The trigger is no longer needed. + "DROP TRIGGER index_data_upgrade_insert_trigger;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // The old table is no longer needed. + "DROP TABLE index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Rename the table. + "ALTER TABLE index_data_upgrade " + "RENAME TO index_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |object_data| table to add the |index_data_values| column, + // remove the ON DELETE CASCADE clause, and apply the WITHOUT ROWID + // optimization. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Insert all the data. + "INSERT INTO object_data_upgrade " + "SELECT " + "object_data.object_store_id, " + "upgrade_key(object_data.key_value), " + "temp_index_data_values.index_data_values, " + "object_data.file_ids, " + "object_data.data " + "FROM object_data " + "LEFT JOIN temp_index_data_values " + "ON object_data.object_store_id = " + "temp_index_data_values.object_store_id " + "AND upgrade_key(object_data.key_value) = " + "temp_index_data_values.key;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // The temporary table is no longer needed. + "DROP TABLE temp_index_data_values;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // The old table is no longer needed. + "DROP TABLE object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + // Rename the table. + "ALTER TABLE object_data_upgrade " + "RENAME TO object_data;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |object_store_index| table to remove the UNIQUE constraint and + // the ON DELETE CASCADE clause. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_store_index_upgrade " + "SELECT * " + "FROM object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE object_store_index_upgrade " + "RENAME TO object_store_index;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |object_store| table to remove the UNIQUE constraint. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_store_upgrade " + "SELECT * " + "FROM object_store;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_store;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE object_store_upgrade " + "RENAME TO object_store;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |database| table to include the origin, vacuum information, and + // apply the WITHOUT ROWID optimization. + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO database_upgrade " + "SELECT name, :origin, version, 0, 0, 0 " + "FROM database;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE database;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE database_upgrade " + "RENAME TO database;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + { + // Make sure there's only one entry in the |database| table. + nsCOMPtr<mozIStorageStatement> stmt; + MOZ_ASSERT(NS_SUCCEEDED( + aConnection->CreateStatement( + NS_LITERAL_CSTRING("SELECT COUNT(*) " + "FROM database;"), + getter_AddRefs(stmt)))); + + bool hasResult; + MOZ_ASSERT(NS_SUCCEEDED(stmt->ExecuteStep(&hasResult))); + + int64_t count; + MOZ_ASSERT(NS_SUCCEEDED(stmt->GetInt64(0, &count))); + + MOZ_ASSERT(count == 1); + } +#endif + + // Recreate file table triggers. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_insert_trigger " + "AFTER INSERT ON object_data " + "WHEN NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(NULL, NEW.file_ids);" + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_update_trigger " + "AFTER UPDATE OF file_ids ON object_data " + "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NEW.file_ids);" + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_delete_trigger " + "AFTER DELETE ON object_data " + "WHEN OLD.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NULL);" + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Finally, turn on auto_vacuum mode. We use full auto_vacuum mode to reclaim + // disk space on mobile devices (at the cost of some COMMIT speed), and + // incremental auto_vacuum mode on desktop builds. + rv = aConnection->ExecuteSimpleSQL( +#ifdef IDB_MOBILE + NS_LITERAL_CSTRING("PRAGMA auto_vacuum = FULL;") +#else + NS_LITERAL_CSTRING("PRAGMA auto_vacuum = INCREMENTAL;") +#endif + ); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(18, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom17_0To18_0(mozIStorageConnection* aConnection, + const nsACString& aOrigin) +{ + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!aOrigin.IsEmpty()); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom17_0To18_0", + js::ProfileEntry::Category::STORAGE); + + return UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(aConnection, aOrigin); +} + +nsresult +UpgradeSchemaFrom18_0To19_0(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsresult rv; + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom18_0To19_0", + js::ProfileEntry::Category::STORAGE); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE object_store_index " + "ADD COLUMN locale TEXT;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE object_store_index " + "ADD COLUMN is_auto_locale BOOLEAN;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE index_data " + "ADD COLUMN value_locale BLOB;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE unique_index_data " + "ADD COLUMN value_locale BLOB;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX index_data_value_locale_index " + "ON index_data (index_id, value_locale, object_data_key, value) " + "WHERE value_locale IS NOT NULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX unique_index_data_value_locale_index " + "ON unique_index_data (index_id, value_locale, object_data_key, value) " + "WHERE value_locale IS NOT NULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(19, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +#if !defined(MOZ_B2G) + +class NormalJSContext; + +class UpgradeFileIdsFunction final + : public mozIStorageFunction +{ + RefPtr<FileManager> mFileManager; + nsAutoPtr<NormalJSContext> mContext; + +public: + UpgradeFileIdsFunction() + { + AssertIsOnIOThread(); + } + + nsresult + Init(nsIFile* aFMDirectory, + mozIStorageConnection* aConnection); + + NS_DECL_ISUPPORTS + +private: + ~UpgradeFileIdsFunction() + { + AssertIsOnIOThread(); + + if (mFileManager) { + mFileManager->Invalidate(); + } + } + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override; +}; + +#endif // MOZ_B2G + +nsresult +UpgradeSchemaFrom19_0To20_0(nsIFile* aFMDirectory, + mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom19_0To20_0", + js::ProfileEntry::Category::STORAGE); + +#if defined(MOZ_B2G) + + // We don't have to do the upgrade of file ids on B2G. The old format was + // only used by the previous single process implementation and B2G was + // always multi process. This is a nice optimization since the upgrade needs + // to deserialize all structured clones which reference a stored file or + // a mutable file. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(20, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#else // MOZ_B2G + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT count(*) " + "FROM object_data " + "WHERE file_ids IS NOT NULL" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t count; + + { + mozStorageStatementScoper scoper(stmt); + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasResult)) { + MOZ_ASSERT(false, "This should never be possible!"); + return NS_ERROR_FAILURE; + } + + count = stmt->AsInt64(0); + if (NS_WARN_IF(count < 0)) { + MOZ_ASSERT(false, "This should never be possible!"); + return NS_ERROR_FAILURE; + } + } + + if (count == 0) { + // Nothing to upgrade. + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(20, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + RefPtr<UpgradeFileIdsFunction> function = new UpgradeFileIdsFunction(); + + rv = function->Init(aFMDirectory, aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_NAMED_LITERAL_CSTRING(functionName, "upgrade"); + + rv = aConnection->CreateFunction(functionName, 2, function); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Disable update trigger. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TRIGGER object_data_update_trigger;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE object_data " + "SET file_ids = upgrade(file_ids, data) " + "WHERE file_ids IS NOT NULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Enable update trigger. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_update_trigger " + "AFTER UPDATE OF file_ids ON object_data " + "FOR EACH ROW " + "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " + "END;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->RemoveFunction(functionName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(20, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#endif // MOZ_B2G + + return NS_OK; +} + +class UpgradeIndexDataValuesFunction final + : public mozIStorageFunction +{ +public: + UpgradeIndexDataValuesFunction() + { + AssertIsOnIOThread(); + } + + NS_DECL_ISUPPORTS + +private: + ~UpgradeIndexDataValuesFunction() + { + AssertIsOnIOThread(); + } + + nsresult + ReadOldCompressedIDVFromBlob(const uint8_t* aBlobData, + uint32_t aBlobDataLength, + nsTArray<IndexDataValue>& aIndexValues); + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override; +}; + +NS_IMPL_ISUPPORTS(UpgradeIndexDataValuesFunction, mozIStorageFunction) + +nsresult +UpgradeIndexDataValuesFunction::ReadOldCompressedIDVFromBlob( + const uint8_t* aBlobData, + uint32_t aBlobDataLength, + nsTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aBlobData); + MOZ_ASSERT(aBlobDataLength); + MOZ_ASSERT(aIndexValues.IsEmpty()); + + const uint8_t* blobDataIter = aBlobData; + const uint8_t* blobDataEnd = aBlobData + aBlobDataLength; + + int64_t indexId; + bool unique; + bool nextIndexIdAlreadyRead = false; + + while (blobDataIter < blobDataEnd) { + if (!nextIndexIdAlreadyRead) { + ReadCompressedIndexId(&blobDataIter, blobDataEnd, &indexId, &unique); + } + nextIndexIdAlreadyRead = false; + + if (NS_WARN_IF(blobDataIter == blobDataEnd)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + // Read key buffer length. + const uint64_t keyBufferLength = + ReadCompressedNumber(&blobDataIter, blobDataEnd); + + if (NS_WARN_IF(blobDataIter == blobDataEnd) || + NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) || + NS_WARN_IF(blobDataIter + keyBufferLength > blobDataEnd)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + nsCString keyBuffer(reinterpret_cast<const char*>(blobDataIter), + uint32_t(keyBufferLength)); + blobDataIter += keyBufferLength; + + IndexDataValue idv(indexId, unique, Key(keyBuffer)); + + if (blobDataIter < blobDataEnd) { + // Read either a sort key buffer length or an index id. + uint64_t maybeIndexId = ReadCompressedNumber(&blobDataIter, blobDataEnd); + + // Locale-aware indexes haven't been around long enough to have any users, + // we can safely assume all sort key buffer lengths will be zero. + if (maybeIndexId != 0) { + if (maybeIndexId % 2) { + unique = true; + maybeIndexId--; + } else { + unique = false; + } + indexId = maybeIndexId/2; + nextIndexIdAlreadyRead = true; + } + } + + if (NS_WARN_IF(!aIndexValues.InsertElementSorted(idv, fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + } + + MOZ_ASSERT(blobDataIter == blobDataEnd); + + return NS_OK; +} + +NS_IMETHODIMP +UpgradeIndexDataValuesFunction::OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) +{ + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + PROFILER_LABEL("IndexedDB", + "UpgradeIndexDataValuesFunction::OnFunctionCall", + js::ProfileEntry::Category::STORAGE); + + uint32_t argc; + nsresult rv = aArguments->GetNumEntries(&argc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (argc != 1) { + NS_WARNING("Don't call me with the wrong number of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + int32_t type; + rv = aArguments->GetTypeOfIndex(0, &type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (type != mozIStorageStatement::VALUE_TYPE_BLOB) { + NS_WARNING("Don't call me with the wrong type of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + const uint8_t* oldBlob; + uint32_t oldBlobLength; + rv = aArguments->GetSharedBlob(0, &oldBlobLength, &oldBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AutoTArray<IndexDataValue, 32> oldIdv; + rv = ReadOldCompressedIDVFromBlob(oldBlob, oldBlobLength, oldIdv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + UniqueFreePtr<uint8_t> newIdv; + uint32_t newIdvLength; + rv = MakeCompressedIndexDataValues(oldIdv, newIdv, &newIdvLength); + + std::pair<uint8_t*, int> data(newIdv.release(), newIdvLength); + + nsCOMPtr<nsIVariant> result = new storage::AdoptedBlobVariant(data); + + result.forget(aResult); + return NS_OK; +} + +nsresult +UpgradeSchemaFrom20_0To21_0(mozIStorageConnection* aConnection) +{ + // This should have been part of the 18 to 19 upgrade, where we changed the + // layout of the index_data_values blobs but didn't upgrade the existing data. + // See bug 1202788. + + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom20_0To21_0", + js::ProfileEntry::Category::STORAGE); + + RefPtr<UpgradeIndexDataValuesFunction> function = + new UpgradeIndexDataValuesFunction(); + + NS_NAMED_LITERAL_CSTRING(functionName, "upgrade_idv"); + + nsresult rv = aConnection->CreateFunction(functionName, 1, function); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE object_data " + "SET index_data_values = upgrade_idv(index_data_values) " + "WHERE index_data_values IS NOT NULL;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->RemoveFunction(functionName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(21, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom21_0To22_0(mozIStorageConnection* aConnection) +{ + // The only change between 21 and 22 was a different structured clone format, + // but it's backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(22, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom22_0To23_0(mozIStorageConnection* aConnection, + const nsACString& aOrigin) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!aOrigin.IsEmpty()); + + PROFILER_LABEL("IndexedDB", + "UpgradeSchemaFrom22_0To23_0", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE database " + "SET origin = :origin;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(23, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom23_0To24_0(mozIStorageConnection* aConnection) +{ + // The only change between 23 and 24 was a different structured clone format, + // but it's backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(24, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +UpgradeSchemaFrom24_0To25_0(mozIStorageConnection* aConnection) +{ + // The changes between 24 and 25 were an upgraded snappy library, a different + // structured clone format and a different file_ds format. But everything is + // backwards-compatible. + nsresult rv = aConnection->SetSchemaVersion(MakeSchemaVersion(25, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +GetDatabaseFileURL(nsIFile* aDatabaseFile, + PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId, + nsIFileURL** aResult) +{ + MOZ_ASSERT(aDatabaseFile); + MOZ_ASSERT(aResult); + + nsresult rv; + + nsCOMPtr<nsIProtocolHandler> protocolHandler( + do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file", &rv)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFileProtocolHandler> fileHandler( + do_QueryInterface(protocolHandler, &rv)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIURI> uri; + rv = fileHandler->NewFileURI(aDatabaseFile, getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri); + MOZ_ASSERT(fileUrl); + + nsAutoCString type; + PersistenceTypeToText(aPersistenceType, type); + + nsAutoCString telemetryFilenameClause; + if (aTelemetryId) { + telemetryFilenameClause.AssignLiteral("&telemetryFilename=indexedDB-"); + telemetryFilenameClause.AppendInt(aTelemetryId); + telemetryFilenameClause.AppendLiteral(".sqlite"); + } + + rv = fileUrl->SetQuery(NS_LITERAL_CSTRING("persistenceType=") + type + + NS_LITERAL_CSTRING("&group=") + aGroup + + NS_LITERAL_CSTRING("&origin=") + aOrigin + + NS_LITERAL_CSTRING("&cache=private") + + telemetryFilenameClause); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + fileUrl.forget(aResult); + return NS_OK; +} + +nsresult +SetDefaultPragmas(mozIStorageConnection* aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aConnection); + + static const char kBuiltInPragmas[] = + // We use foreign keys in DEBUG builds only because there is a performance + // cost to using them. + "PRAGMA foreign_keys = " +#ifdef DEBUG + "ON" +#else + "OFF" +#endif + ";" + + // The "INSERT OR REPLACE" statement doesn't fire the update trigger, + // instead it fires only the insert trigger. This confuses the update + // refcount function. This behavior changes with enabled recursive triggers, + // so the statement fires the delete trigger first and then the insert + // trigger. + "PRAGMA recursive_triggers = ON;" + + // We aggressively truncate the database file when idle so don't bother + // overwriting the WAL with 0 during active periods. + "PRAGMA secure_delete = OFF;" + ; + + nsresult rv = + aConnection->ExecuteSimpleSQL( + nsDependentCString(kBuiltInPragmas, + LiteralStringLength(kBuiltInPragmas))); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString pragmaStmt; + pragmaStmt.AssignLiteral("PRAGMA synchronous = "); + + if (IndexedDatabaseManager::FullSynchronous()) { + pragmaStmt.AppendLiteral("FULL"); + } else { + pragmaStmt.AppendLiteral("NORMAL"); + } + pragmaStmt.Append(';'); + + rv = aConnection->ExecuteSimpleSQL(pragmaStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifndef IDB_MOBILE + if (kSQLiteGrowthIncrement) { + // This is just an optimization so ignore the failure if the disk is + // currently too full. + rv = aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, + EmptyCString()); + if (rv != NS_ERROR_FILE_TOO_BIG && NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } +#endif // IDB_MOBILE + + return NS_OK; +} + +nsresult +SetJournalMode(mozIStorageConnection* aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aConnection); + + // Try enabling WAL mode. This can fail in various circumstances so we have to + // check the results here. + NS_NAMED_LITERAL_CSTRING(journalModeQueryStart, "PRAGMA journal_mode = "); + NS_NAMED_LITERAL_CSTRING(journalModeWAL, "wal"); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = + aConnection->CreateStatement(journalModeQueryStart + journalModeWAL, + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + nsCString journalMode; + rv = stmt->GetUTF8String(0, journalMode); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (journalMode.Equals(journalModeWAL)) { + // WAL mode successfully enabled. Maybe set limits on its size here. + if (kMaxWALPages >= 0) { + nsAutoCString pageCount; + pageCount.AppendInt(kMaxWALPages); + + rv = aConnection->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("PRAGMA wal_autocheckpoint = ") + pageCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } else { + NS_WARNING("Failed to set WAL mode, falling back to normal journal mode."); +#ifdef IDB_MOBILE + rv = aConnection->ExecuteSimpleSQL(journalModeQueryStart + + NS_LITERAL_CSTRING("truncate")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } +#endif + } + + return NS_OK; +} + +template <class FileOrURLType> +struct StorageOpenTraits; + +template <> +struct StorageOpenTraits<nsIFileURL*> +{ + static nsresult + Open(mozIStorageService* aStorageService, + nsIFileURL* aFileURL, + mozIStorageConnection** aConnection) + { + return aStorageService->OpenDatabaseWithFileURL(aFileURL, aConnection); + } + +#ifdef DEBUG + static void + GetPath(nsIFileURL* aFileURL, nsCString& aPath) + { + MOZ_ALWAYS_SUCCEEDS(aFileURL->GetFileName(aPath)); + } +#endif +}; + +template <> +struct StorageOpenTraits<nsIFile*> +{ + static nsresult + Open(mozIStorageService* aStorageService, + nsIFile* aFile, + mozIStorageConnection** aConnection) + { + return aStorageService->OpenUnsharedDatabase(aFile, aConnection); + } + +#ifdef DEBUG + static void + GetPath(nsIFile* aFile, nsCString& aPath) + { + nsString path; + MOZ_ALWAYS_SUCCEEDS(aFile->GetPath(path)); + + aPath.AssignWithConversion(path); + } +#endif +}; + +template <template <class> class SmartPtr, class FileOrURLType> +struct StorageOpenTraits<SmartPtr<FileOrURLType>> + : public StorageOpenTraits<FileOrURLType*> +{ }; + +template <class FileOrURLType> +nsresult +OpenDatabaseAndHandleBusy(mozIStorageService* aStorageService, + FileOrURLType aFileOrURL, + mozIStorageConnection** aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aStorageService); + MOZ_ASSERT(aFileOrURL); + MOZ_ASSERT(aConnection); + + nsCOMPtr<mozIStorageConnection> connection; + nsresult rv = + StorageOpenTraits<FileOrURLType>::Open(aStorageService, + aFileOrURL, + getter_AddRefs(connection)); + + if (rv == NS_ERROR_STORAGE_BUSY) { +#ifdef DEBUG + { + nsCString path; + StorageOpenTraits<FileOrURLType>::GetPath(aFileOrURL, path); + + nsPrintfCString message("Received NS_ERROR_STORAGE_BUSY when attempting " + "to open database '%s', retrying for up to 10 " + "seconds", + path.get()); + NS_WARNING(message.get()); + } +#endif + + // Another thread must be checkpointing the WAL. Wait up to 10 seconds for + // that to complete. + TimeStamp start = TimeStamp::NowLoRes(); + + while (true) { + PR_Sleep(PR_MillisecondsToInterval(100)); + + rv = StorageOpenTraits<FileOrURLType>::Open(aStorageService, + aFileOrURL, + getter_AddRefs(connection)); + if (rv != NS_ERROR_STORAGE_BUSY || + TimeStamp::NowLoRes() - start > TimeDuration::FromSeconds(10)) { + break; + } + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +CreateStorageConnection(nsIFile* aDBFile, + nsIFile* aFMDirectory, + const nsAString& aName, + PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId, + mozIStorageConnection** aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDBFile); + MOZ_ASSERT(aFMDirectory); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "CreateStorageConnection", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + bool exists; + + if (IndexedDatabaseManager::InLowDiskSpaceMode()) { + rv = aDBFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + NS_WARNING("Refusing to create database because disk space is low!"); + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + } + + nsCOMPtr<nsIFileURL> dbFileUrl; + rv = GetDatabaseFileURL(aDBFile, + aPersistenceType, + aGroup, + aOrigin, + aTelemetryId, + getter_AddRefs(dbFileUrl)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageConnection> connection; + rv = OpenDatabaseAndHandleBusy(ss, dbFileUrl, getter_AddRefs(connection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + // If we're just opening the database during origin initialization, then + // we don't want to erase any files. The failure here will fail origin + // initialization too. + if (aName.IsVoid()) { + return rv; + } + + // Nuke the database file. + rv = aDBFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aFMDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + bool isDirectory; + rv = aFMDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (NS_WARN_IF(!isDirectory)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + rv = aFMDirectory->Remove(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = OpenDatabaseAndHandleBusy(ss, dbFileUrl, getter_AddRefs(connection)); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetDefaultPragmas(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = connection->EnableModule(NS_LITERAL_CSTRING("filesystem")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Check to make sure that the database schema is correct. + int32_t schemaVersion; + rv = connection->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Unknown schema will fail origin initialization too. + if (!schemaVersion && aName.IsVoid()) { + IDB_WARNING("Unable to open IndexedDB database, schema is not set!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (schemaVersion > kSQLiteSchemaVersion) { + IDB_WARNING("Unable to open IndexedDB database, schema is too high!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + bool journalModeSet = false; + + if (schemaVersion != kSQLiteSchemaVersion) { + const bool newDatabase = !schemaVersion; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + rv = connection->ExecuteSimpleSQL( + nsPrintfCString("PRAGMA page_size = %lu;", kSQLitePageSizeOverride) + ); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // We have to set the auto_vacuum mode before opening a transaction. + rv = connection->ExecuteSimpleSQL( +#ifdef IDB_MOBILE + // Turn on full auto_vacuum mode to reclaim disk space on mobile + // devices (at the cost of some COMMIT speed). + NS_LITERAL_CSTRING("PRAGMA auto_vacuum = FULL;") +#else + // Turn on incremental auto_vacuum mode on desktop builds. + NS_LITERAL_CSTRING("PRAGMA auto_vacuum = INCREMENTAL;") +#endif + ); + if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { + // mozstorage translates SQLITE_FULL to NS_ERROR_FILE_NO_DEVICE_SPACE, + // which we know better as NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR. + rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetJournalMode(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + journalModeSet = true; + } else { +#ifdef DEBUG + // Disable foreign key support while upgrading. This has to be done before + // starting a transaction. + MOZ_ALWAYS_SUCCEEDS( + connection->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("PRAGMA foreign_keys = OFF;"))); +#endif + } + + bool vacuumNeeded = false; + + mozStorageTransaction transaction(connection, false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + if (newDatabase) { + rv = CreateTables(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(NS_SUCCEEDED(connection->GetSchemaVersion(&schemaVersion))); + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO database (name, origin) " + "VALUES (:name, :origin)" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), aName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("origin"), aOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // This logic needs to change next time we change the schema! + static_assert(kSQLiteSchemaVersion == int32_t((25 << 4) + 0), + "Upgrade function needed due to schema version increase."); + + while (schemaVersion != kSQLiteSchemaVersion) { + if (schemaVersion == 4) { + rv = UpgradeSchemaFrom4To5(connection); + } else if (schemaVersion == 5) { + rv = UpgradeSchemaFrom5To6(connection); + } else if (schemaVersion == 6) { + rv = UpgradeSchemaFrom6To7(connection); + } else if (schemaVersion == 7) { + rv = UpgradeSchemaFrom7To8(connection); + } else if (schemaVersion == 8) { + rv = UpgradeSchemaFrom8To9_0(connection); + vacuumNeeded = true; + } else if (schemaVersion == MakeSchemaVersion(9, 0)) { + rv = UpgradeSchemaFrom9_0To10_0(connection); + } else if (schemaVersion == MakeSchemaVersion(10, 0)) { + rv = UpgradeSchemaFrom10_0To11_0(connection); + } else if (schemaVersion == MakeSchemaVersion(11, 0)) { + rv = UpgradeSchemaFrom11_0To12_0(connection); + } else if (schemaVersion == MakeSchemaVersion(12, 0)) { + rv = UpgradeSchemaFrom12_0To13_0(connection, &vacuumNeeded); + } else if (schemaVersion == MakeSchemaVersion(13, 0)) { + rv = UpgradeSchemaFrom13_0To14_0(connection); + } else if (schemaVersion == MakeSchemaVersion(14, 0)) { + rv = UpgradeSchemaFrom14_0To15_0(connection); + } else if (schemaVersion == MakeSchemaVersion(15, 0)) { + rv = UpgradeSchemaFrom15_0To16_0(connection); + } else if (schemaVersion == MakeSchemaVersion(16, 0)) { + rv = UpgradeSchemaFrom16_0To17_0(connection); + } else if (schemaVersion == MakeSchemaVersion(17, 0)) { + rv = UpgradeSchemaFrom17_0To18_0(connection, aOrigin); + vacuumNeeded = true; + } else if (schemaVersion == MakeSchemaVersion(18, 0)) { + rv = UpgradeSchemaFrom18_0To19_0(connection); + } else if (schemaVersion == MakeSchemaVersion(19, 0)) { + rv = UpgradeSchemaFrom19_0To20_0(aFMDirectory, connection); + } else if (schemaVersion == MakeSchemaVersion(20, 0)) { + rv = UpgradeSchemaFrom20_0To21_0(connection); + } else if (schemaVersion == MakeSchemaVersion(21, 0)) { + rv = UpgradeSchemaFrom21_0To22_0(connection); + } else if (schemaVersion == MakeSchemaVersion(22, 0)) { + rv = UpgradeSchemaFrom22_0To23_0(connection, aOrigin); + } else if (schemaVersion == MakeSchemaVersion(23, 0)) { + rv = UpgradeSchemaFrom23_0To24_0(connection); + } else if (schemaVersion == MakeSchemaVersion(24, 0)) { + rv = UpgradeSchemaFrom24_0To25_0(connection); + } else { + IDB_WARNING("Unable to open IndexedDB database, no upgrade path is " + "available!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = connection->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } + + rv = transaction.Commit(); + if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { + // mozstorage translates SQLITE_FULL to NS_ERROR_FILE_NO_DEVICE_SPACE, + // which we know better as NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR. + rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + if (!newDatabase) { + // Re-enable foreign key support after doing a foreign key check. + nsCOMPtr<mozIStorageStatement> checkStmt; + MOZ_ALWAYS_SUCCEEDS( + connection->CreateStatement( + NS_LITERAL_CSTRING("PRAGMA foreign_key_check;"), + getter_AddRefs(checkStmt))); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(checkStmt->ExecuteStep(&hasResult)); + MOZ_ASSERT(!hasResult, "Database has inconsisistent foreign keys!"); + + MOZ_ALWAYS_SUCCEEDS( + connection->ExecuteSimpleSQL( + NS_LITERAL_CSTRING("PRAGMA foreign_keys = OFF;"))); + } +#endif + + if (kSQLitePageSizeOverride && !newDatabase) { + nsCOMPtr<mozIStorageStatement> stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA page_size;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + int32_t pageSize; + rv = stmt->GetInt32(0, &pageSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); + + if (kSQLitePageSizeOverride != uint32_t(pageSize)) { + // We must not be in WAL journal mode to change the page size. + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA journal_mode = DELETE;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA journal_mode;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + nsCString journalMode; + rv = stmt->GetUTF8String(0, journalMode); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (journalMode.EqualsLiteral("delete")) { + // Successfully set to rollback journal mode so changing the page size + // is possible with a VACUUM. + rv = connection->ExecuteSimpleSQL( + nsPrintfCString("PRAGMA page_size = %lu;", kSQLitePageSizeOverride) + ); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We will need to VACUUM in order to change the page size. + vacuumNeeded = true; + } else { + NS_WARNING("Failed to set journal_mode for database, unable to " + "change the page size!"); + } + } + } + + if (vacuumNeeded) { + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("VACUUM;")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (newDatabase || vacuumNeeded) { + if (journalModeSet) { + // Make sure we checkpoint to get an accurate file size. + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA wal_checkpoint(FULL);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + int64_t fileSize; + rv = aDBFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(fileSize > 0); + + PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime); + + nsCOMPtr<mozIStorageStatement> vacuumTimeStmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;" + ), getter_AddRefs(vacuumTimeStmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("time"), + vacuumTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = vacuumTimeStmt->BindInt64ByName(NS_LITERAL_CSTRING("size"), + fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = vacuumTimeStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + if (!journalModeSet) { + rv = SetJournalMode(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + connection.forget(aConnection); + return NS_OK; +} + +already_AddRefed<nsIFile> +GetFileForPath(const nsAString& aPath) +{ + MOZ_ASSERT(!aPath.IsEmpty()); + + nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); + if (NS_WARN_IF(!file)) { + return nullptr; + } + + if (NS_WARN_IF(NS_FAILED(file->InitWithPath(aPath)))) { + return nullptr; + } + + return file.forget(); +} + +nsresult +GetStorageConnection(nsIFile* aDatabaseFile, + PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId, + mozIStorageConnection** aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aDatabaseFile); + MOZ_ASSERT(aConnection); + + PROFILER_LABEL("IndexedDB", + "GetStorageConnection", + js::ProfileEntry::Category::STORAGE); + + bool exists; + nsresult rv = aDatabaseFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!exists)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsCOMPtr<nsIFileURL> dbFileUrl; + rv = GetDatabaseFileURL(aDatabaseFile, + aPersistenceType, + aGroup, + aOrigin, + aTelemetryId, + getter_AddRefs(dbFileUrl)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageConnection> connection; + rv = OpenDatabaseAndHandleBusy(ss, dbFileUrl, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetDefaultPragmas(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = SetJournalMode(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + connection.forget(aConnection); + return NS_OK; +} + +nsresult +GetStorageConnection(const nsAString& aDatabaseFilePath, + PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId, + mozIStorageConnection** aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, NS_LITERAL_STRING(".sqlite"))); + MOZ_ASSERT(aConnection); + + nsCOMPtr<nsIFile> dbFile = GetFileForPath(aDatabaseFilePath); + if (NS_WARN_IF(!dbFile)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return GetStorageConnection(dbFile, + aPersistenceType, + aGroup, + aOrigin, + aTelemetryId, + aConnection); +} + +/******************************************************************************* + * ConnectionPool declarations + ******************************************************************************/ + +class DatabaseConnection final +{ + friend class ConnectionPool; + + enum class CheckpointMode + { + Full, + Restart, + Truncate + }; + +public: + class AutoSavepoint; + class CachedStatement; + class UpdateRefcountFunction; + +private: + nsCOMPtr<mozIStorageConnection> mStorageConnection; + RefPtr<FileManager> mFileManager; + nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement> + mCachedStatements; + RefPtr<UpdateRefcountFunction> mUpdateRefcountFunction; + RefPtr<QuotaObject> mQuotaObject; + RefPtr<QuotaObject> mJournalQuotaObject; + bool mInReadTransaction; + bool mInWriteTransaction; + +#ifdef DEBUG + uint32_t mDEBUGSavepointCount; + PRThread* mDEBUGThread; +#endif + +public: + void + AssertIsOnConnectionThread() const + { + MOZ_ASSERT(mDEBUGThread); + MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGThread); + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection) + + mozIStorageConnection* + GetStorageConnection() const + { + if (mStorageConnection) { + AssertIsOnConnectionThread(); + return mStorageConnection; + } + + return nullptr; + } + + UpdateRefcountFunction* + GetUpdateRefcountFunction() const + { + AssertIsOnConnectionThread(); + + return mUpdateRefcountFunction; + } + + nsresult + GetCachedStatement(const nsACString& aQuery, + CachedStatement* aCachedStatement); + + nsresult + BeginWriteTransaction(); + + nsresult + CommitWriteTransaction(); + + void + RollbackWriteTransaction(); + + void + FinishWriteTransaction(); + + nsresult + StartSavepoint(); + + nsresult + ReleaseSavepoint(); + + nsresult + RollbackSavepoint(); + + nsresult + Checkpoint() + { + AssertIsOnConnectionThread(); + + return CheckpointInternal(CheckpointMode::Full); + } + + void + DoIdleProcessing(bool aNeedsCheckpoint); + + void + Close(); + + nsresult + DisableQuotaChecks(); + + void + EnableQuotaChecks(); + +private: + DatabaseConnection(mozIStorageConnection* aStorageConnection, + FileManager* aFileManager); + + ~DatabaseConnection(); + + nsresult + Init(); + + nsresult + CheckpointInternal(CheckpointMode aMode); + + nsresult + GetFreelistCount(CachedStatement& aCachedStatement, uint32_t* aFreelistCount); + + nsresult + ReclaimFreePagesWhileIdle(CachedStatement& aFreelistStatement, + CachedStatement& aRollbackStatement, + uint32_t aFreelistCount, + bool aNeedsCheckpoint, + bool* aFreedSomePages); + + nsresult + GetFileSize(const nsAString& aPath, int64_t* aResult); +}; + +class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final +{ + DatabaseConnection* mConnection; +#ifdef DEBUG + const TransactionBase* mDEBUGTransaction; +#endif + +public: + AutoSavepoint(); + ~AutoSavepoint(); + + nsresult + Start(const TransactionBase* aConnection); + + nsresult + Commit(); +}; + +class DatabaseConnection::CachedStatement final +{ + friend class DatabaseConnection; + + nsCOMPtr<mozIStorageStatement> mStatement; + Maybe<mozStorageStatementScoper> mScoper; + +#ifdef DEBUG + DatabaseConnection* mDEBUGConnection; +#endif + +public: + CachedStatement(); + ~CachedStatement(); + + void + AssertIsOnConnectionThread() const + { +#ifdef DEBUG + if (mDEBUGConnection) { + mDEBUGConnection->AssertIsOnConnectionThread(); + } + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); +#endif + } + + operator mozIStorageStatement*() const; + + mozIStorageStatement* + operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN; + + void + Reset(); + +private: + // Only called by DatabaseConnection. + void + Assign(DatabaseConnection* aConnection, + already_AddRefed<mozIStorageStatement> aStatement); + + // No funny business allowed. + CachedStatement(const CachedStatement&) = delete; + CachedStatement& operator=(const CachedStatement&) = delete; +}; + +class DatabaseConnection::UpdateRefcountFunction final + : public mozIStorageFunction +{ + class DatabaseUpdateFunction; + class FileInfoEntry; + + enum class UpdateType + { + Increment, + Decrement + }; + + DatabaseConnection* mConnection; + FileManager* mFileManager; + nsClassHashtable<nsUint64HashKey, FileInfoEntry> mFileInfoEntries; + nsDataHashtable<nsUint64HashKey, FileInfoEntry*> mSavepointEntriesIndex; + + nsTArray<int64_t> mJournalsToCreateBeforeCommit; + nsTArray<int64_t> mJournalsToRemoveAfterCommit; + nsTArray<int64_t> mJournalsToRemoveAfterAbort; + + bool mInSavepoint; + +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + + UpdateRefcountFunction(DatabaseConnection* aConnection, + FileManager* aFileManager); + + nsresult + WillCommit(); + + void + DidCommit(); + + void + DidAbort(); + + void + StartSavepoint(); + + void + ReleaseSavepoint(); + + void + RollbackSavepoint(); + + void + Reset(); + +private: + ~UpdateRefcountFunction() + { } + + nsresult + ProcessValue(mozIStorageValueArray* aValues, + int32_t aIndex, + UpdateType aUpdateType); + + nsresult + CreateJournals(); + + nsresult + RemoveJournals(const nsTArray<int64_t>& aJournals); +}; + +class DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction final +{ + CachedStatement mUpdateStatement; + CachedStatement mSelectStatement; + CachedStatement mInsertStatement; + + UpdateRefcountFunction* mFunction; + + nsresult mErrorCode; + +public: + explicit + DatabaseUpdateFunction(UpdateRefcountFunction* aFunction) + : mFunction(aFunction) + , mErrorCode(NS_OK) + { + MOZ_COUNT_CTOR( + DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction); + } + + ~DatabaseUpdateFunction() + { + MOZ_COUNT_DTOR( + DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction); + } + + bool + Update(int64_t aId, int32_t aDelta); + + nsresult + ErrorCode() const + { + return mErrorCode; + } + +private: + nsresult + UpdateInternal(int64_t aId, int32_t aDelta); +}; + +class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final +{ + friend class UpdateRefcountFunction; + + RefPtr<FileInfo> mFileInfo; + int32_t mDelta; + int32_t mSavepointDelta; + +public: + explicit + FileInfoEntry(FileInfo* aFileInfo) + : mFileInfo(aFileInfo) + , mDelta(0) + , mSavepointDelta(0) + { + MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); + } + + ~FileInfoEntry() + { + MOZ_COUNT_DTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); + } +}; + +class ConnectionPool final +{ +public: + class FinishCallback; + +private: + class ConnectionRunnable; + class CloseConnectionRunnable; + struct DatabaseInfo; + struct DatabasesCompleteCallback; + class FinishCallbackWrapper; + class IdleConnectionRunnable; + struct IdleDatabaseInfo; + struct IdleResource; + struct IdleThreadInfo; + struct ThreadInfo; + class ThreadRunnable; + class TransactionInfo; + struct TransactionInfoPair; + + // This mutex guards mDatabases, see below. + Mutex mDatabasesMutex; + + nsTArray<IdleThreadInfo> mIdleThreads; + nsTArray<IdleDatabaseInfo> mIdleDatabases; + nsTArray<DatabaseInfo*> mDatabasesPerformingIdleMaintenance; + nsCOMPtr<nsITimer> mIdleTimer; + TimeStamp mTargetIdleTime; + + // Only modifed on the owning thread, but read on multiple threads. Therefore + // all modifications and all reads off the owning thread must be protected by + // mDatabasesMutex. + nsClassHashtable<nsCStringHashKey, DatabaseInfo> mDatabases; + + nsClassHashtable<nsUint64HashKey, TransactionInfo> mTransactions; + nsTArray<TransactionInfo*> mQueuedTransactions; + + nsTArray<nsAutoPtr<DatabasesCompleteCallback>> mCompleteCallbacks; + + uint64_t mNextTransactionId; + uint32_t mTotalThreadCount; + bool mShutdownRequested; + bool mShutdownComplete; + +#ifdef DEBUG + PRThread* mDEBUGOwningThread; +#endif + +public: + ConnectionPool(); + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + nsresult + GetOrCreateConnection(const Database* aDatabase, + DatabaseConnection** aConnection); + + uint64_t + Start(const nsID& aBackgroundChildLoggingId, + const nsACString& aDatabaseId, + int64_t aLoggingSerialNumber, + const nsTArray<nsString>& aObjectStoreNames, + bool aIsWriteTransaction, + TransactionDatabaseOperationBase* aTransactionOp); + + void + Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable); + + void + Finish(uint64_t aTransactionId, FinishCallback* aCallback); + + void + CloseDatabaseWhenIdle(const nsACString& aDatabaseId) + { + Unused << CloseDatabaseWhenIdleInternal(aDatabaseId); + } + + void + WaitForDatabasesToComplete(nsTArray<nsCString>&& aDatabaseIds, + nsIRunnable* aCallback); + + void + Shutdown(); + + NS_INLINE_DECL_REFCOUNTING(ConnectionPool) + +private: + ~ConnectionPool(); + + static void + IdleTimerCallback(nsITimer* aTimer, void* aClosure); + + void + Cleanup(); + + void + AdjustIdleTimer(); + + void + CancelIdleTimer(); + + void + ShutdownThread(ThreadInfo& aThreadInfo); + + void + CloseIdleDatabases(); + + void + ShutdownIdleThreads(); + + bool + ScheduleTransaction(TransactionInfo* aTransactionInfo, + bool aFromQueuedTransactions); + + void + NoteFinishedTransaction(uint64_t aTransactionId); + + void + ScheduleQueuedTransactions(ThreadInfo& aThreadInfo); + + void + NoteIdleDatabase(DatabaseInfo* aDatabaseInfo); + + void + NoteClosedDatabase(DatabaseInfo* aDatabaseInfo); + + bool + MaybeFireCallback(DatabasesCompleteCallback* aCallback); + + void + PerformIdleDatabaseMaintenance(DatabaseInfo* aDatabaseInfo); + + void + CloseDatabase(DatabaseInfo* aDatabaseInfo); + + bool + CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId); +}; + +class ConnectionPool::ConnectionRunnable + : public Runnable +{ +protected: + DatabaseInfo* mDatabaseInfo; + nsCOMPtr<nsIEventTarget> mOwningThread; + + explicit + ConnectionRunnable(DatabaseInfo* aDatabaseInfo); + + virtual + ~ConnectionRunnable() + { } +}; + +class ConnectionPool::IdleConnectionRunnable final + : public ConnectionRunnable +{ + bool mNeedsCheckpoint; + +public: + IdleConnectionRunnable(DatabaseInfo* aDatabaseInfo, bool aNeedsCheckpoint) + : ConnectionRunnable(aDatabaseInfo) + , mNeedsCheckpoint(aNeedsCheckpoint) + { } + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~IdleConnectionRunnable() + { } + + NS_DECL_NSIRUNNABLE +}; + +class ConnectionPool::CloseConnectionRunnable final + : public ConnectionRunnable +{ +public: + explicit + CloseConnectionRunnable(DatabaseInfo* aDatabaseInfo) + : ConnectionRunnable(aDatabaseInfo) + { } + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~CloseConnectionRunnable() + { } + + NS_DECL_NSIRUNNABLE +}; + +struct ConnectionPool::ThreadInfo +{ + nsCOMPtr<nsIThread> mThread; + RefPtr<ThreadRunnable> mRunnable; + + ThreadInfo(); + + explicit + ThreadInfo(const ThreadInfo& aOther); + + ~ThreadInfo(); +}; + +struct ConnectionPool::DatabaseInfo final +{ + friend class nsAutoPtr<DatabaseInfo>; + + RefPtr<ConnectionPool> mConnectionPool; + const nsCString mDatabaseId; + RefPtr<DatabaseConnection> mConnection; + nsClassHashtable<nsStringHashKey, TransactionInfoPair> mBlockingTransactions; + nsTArray<TransactionInfo*> mTransactionsScheduledDuringClose; + nsTArray<TransactionInfo*> mScheduledWriteTransactions; + TransactionInfo* mRunningWriteTransaction; + ThreadInfo mThreadInfo; + uint32_t mReadTransactionCount; + uint32_t mWriteTransactionCount; + bool mNeedsCheckpoint; + bool mIdle; + bool mCloseOnIdle; + bool mClosing; + +#ifdef DEBUG + PRThread* mDEBUGConnectionThread; +#endif + + DatabaseInfo(ConnectionPool* aConnectionPool, + const nsACString& aDatabaseId); + + void + AssertIsOnConnectionThread() const + { + MOZ_ASSERT(mDEBUGConnectionThread); + MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGConnectionThread); + } + + uint64_t + TotalTransactionCount() const + { + return mReadTransactionCount + mWriteTransactionCount; + } + +private: + ~DatabaseInfo(); + + DatabaseInfo(const DatabaseInfo&) = delete; + DatabaseInfo& operator=(const DatabaseInfo&) = delete; +}; + +struct ConnectionPool::DatabasesCompleteCallback final +{ + friend class nsAutoPtr<DatabasesCompleteCallback>; + + nsTArray<nsCString> mDatabaseIds; + nsCOMPtr<nsIRunnable> mCallback; + + DatabasesCompleteCallback(nsTArray<nsCString>&& aDatabaseIds, + nsIRunnable* aCallback); + +private: + ~DatabasesCompleteCallback(); +}; + +class NS_NO_VTABLE ConnectionPool::FinishCallback + : public nsIRunnable +{ +public: + // Called on the owning thread before any additional transactions are + // unblocked. + virtual void + TransactionFinishedBeforeUnblock() = 0; + + // Called on the owning thread after additional transactions may have been + // unblocked. + virtual void + TransactionFinishedAfterUnblock() = 0; + +protected: + FinishCallback() + { } + + virtual ~FinishCallback() + { } +}; + +class ConnectionPool::FinishCallbackWrapper final + : public Runnable +{ + RefPtr<ConnectionPool> mConnectionPool; + RefPtr<FinishCallback> mCallback; + nsCOMPtr<nsIEventTarget> mOwningThread; + uint64_t mTransactionId; + bool mHasRunOnce; + +public: + FinishCallbackWrapper(ConnectionPool* aConnectionPool, + uint64_t aTransactionId, + FinishCallback* aCallback); + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~FinishCallbackWrapper(); + + NS_DECL_NSIRUNNABLE +}; + +struct ConnectionPool::IdleResource +{ + TimeStamp mIdleTime; + +protected: + explicit + IdleResource(const TimeStamp& aIdleTime); + + explicit + IdleResource(const IdleResource& aOther) = delete; + + ~IdleResource(); +}; + +struct ConnectionPool::IdleDatabaseInfo final + : public IdleResource +{ + DatabaseInfo* mDatabaseInfo; + +public: + MOZ_IMPLICIT + IdleDatabaseInfo(DatabaseInfo* aDatabaseInfo); + + explicit + IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete; + + ~IdleDatabaseInfo(); + + bool + operator==(const IdleDatabaseInfo& aOther) const + { + return mDatabaseInfo == aOther.mDatabaseInfo; + } + + bool + operator<(const IdleDatabaseInfo& aOther) const + { + return mIdleTime < aOther.mIdleTime; + } +}; + +struct ConnectionPool::IdleThreadInfo final + : public IdleResource +{ + ThreadInfo mThreadInfo; + +public: + // Boo, this is needed because nsTArray::InsertElementSorted() doesn't yet + // work with rvalue references. + MOZ_IMPLICIT + IdleThreadInfo(const ThreadInfo& aThreadInfo); + + explicit + IdleThreadInfo(const IdleThreadInfo& aOther) = delete; + + ~IdleThreadInfo(); + + bool + operator==(const IdleThreadInfo& aOther) const + { + return mThreadInfo.mRunnable == aOther.mThreadInfo.mRunnable && + mThreadInfo.mThread == aOther.mThreadInfo.mThread; + } + + bool + operator<(const IdleThreadInfo& aOther) const + { + return mIdleTime < aOther.mIdleTime; + } +}; + +class ConnectionPool::ThreadRunnable final + : public Runnable +{ + // Only touched on the background thread. + static uint32_t sNextSerialNumber; + + // Set at construction for logging. + const uint32_t mSerialNumber; + + // These two values are only modified on the connection thread. + bool mFirstRun; + bool mContinueRunning; + +public: + ThreadRunnable(); + + NS_DECL_ISUPPORTS_INHERITED + + uint32_t + SerialNumber() const + { + return mSerialNumber; + } + +private: + ~ThreadRunnable(); + + NS_DECL_NSIRUNNABLE +}; + +class ConnectionPool::TransactionInfo final +{ + friend class nsAutoPtr<TransactionInfo>; + + nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlocking; + nsTArray<TransactionInfo*> mBlockingOrdered; + +public: + DatabaseInfo* mDatabaseInfo; + const nsID mBackgroundChildLoggingId; + const nsCString mDatabaseId; + const uint64_t mTransactionId; + const int64_t mLoggingSerialNumber; + const nsTArray<nsString> mObjectStoreNames; + nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlockedOn; + nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables; + const bool mIsWriteTransaction; + bool mRunning; + +#ifdef DEBUG + bool mFinished; +#endif + + TransactionInfo(DatabaseInfo* aDatabaseInfo, + const nsID& aBackgroundChildLoggingId, + const nsACString& aDatabaseId, + uint64_t aTransactionId, + int64_t aLoggingSerialNumber, + const nsTArray<nsString>& aObjectStoreNames, + bool aIsWriteTransaction, + TransactionDatabaseOperationBase* aTransactionOp); + + void + AddBlockingTransaction(TransactionInfo* aTransactionInfo); + + void + RemoveBlockingTransactions(); + +private: + ~TransactionInfo(); + + void + MaybeUnblock(TransactionInfo* aTransactionInfo); +}; + +struct ConnectionPool::TransactionInfoPair final +{ + friend class nsAutoPtr<TransactionInfoPair>; + + // Multiple reading transactions can block future writes. + nsTArray<TransactionInfo*> mLastBlockingWrites; + // But only a single writing transaction can block future reads. + TransactionInfo* mLastBlockingReads; + + TransactionInfoPair(); + +private: + ~TransactionInfoPair(); +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +class DatabaseOperationBase + : public Runnable + , public mozIStorageProgressHandler +{ + friend class UpgradeFileIdsFunction; + +protected: + class AutoSetProgressHandler; + + typedef nsDataHashtable<nsUint64HashKey, bool> UniqueIndexTable; + + nsCOMPtr<nsIEventTarget> mOwningThread; + const nsID mBackgroundChildLoggingId; + const uint64_t mLoggingSerialNumber; + nsresult mResultCode; + +private: + Atomic<bool> mOperationMayProceed; + bool mActorDestroyed; + +public: + NS_DECL_ISUPPORTS_INHERITED + + bool + IsOnOwningThread() const + { + MOZ_ASSERT(mOwningThread); + + bool current; + return NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t)) && current; + } + + void + AssertIsOnOwningThread() const + { + MOZ_ASSERT(IsOnBackgroundThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + void + NoteActorDestroyed() + { + AssertIsOnOwningThread(); + + mActorDestroyed = true; + mOperationMayProceed = false; + } + + bool + IsActorDestroyed() const + { + AssertIsOnOwningThread(); + + return mActorDestroyed; + } + + // May be called on any thread, but you should call IsActorDestroyed() if + // you know you're on the background thread because it is slightly faster. + bool + OperationMayProceed() const + { + return mOperationMayProceed; + } + + const nsID& + BackgroundChildLoggingId() const + { + return mBackgroundChildLoggingId; + } + + uint64_t + LoggingSerialNumber() const + { + return mLoggingSerialNumber; + } + + nsresult + ResultCode() const + { + return mResultCode; + } + + void + SetFailureCode(nsresult aErrorCode) + { + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + + mResultCode = aErrorCode; + } + +protected: + DatabaseOperationBase(const nsID& aBackgroundChildLoggingId, + uint64_t aLoggingSerialNumber) + : mOwningThread(NS_GetCurrentThread()) + , mBackgroundChildLoggingId(aBackgroundChildLoggingId) + , mLoggingSerialNumber(aLoggingSerialNumber) + , mResultCode(NS_OK) + , mOperationMayProceed(true) + , mActorDestroyed(false) + { + AssertIsOnOwningThread(); + } + + virtual + ~DatabaseOperationBase() + { + MOZ_ASSERT(mActorDestroyed); + } + + static void + GetBindingClauseForKeyRange(const SerializedKeyRange& aKeyRange, + const nsACString& aKeyColumnName, + nsAutoCString& aBindingClause); + + static uint64_t + ReinterpretDoubleAsUInt64(double aDouble); + + static nsresult + GetStructuredCloneReadInfoFromStatement(mozIStorageStatement* aStatement, + uint32_t aDataIndex, + uint32_t aFileIdsIndex, + FileManager* aFileManager, + StructuredCloneReadInfo* aInfo) + { + return GetStructuredCloneReadInfoFromSource(aStatement, + aDataIndex, + aFileIdsIndex, + aFileManager, + aInfo); + } + + static nsresult + GetStructuredCloneReadInfoFromValueArray(mozIStorageValueArray* aValues, + uint32_t aDataIndex, + uint32_t aFileIdsIndex, + FileManager* aFileManager, + StructuredCloneReadInfo* aInfo) + { + return GetStructuredCloneReadInfoFromSource(aValues, + aDataIndex, + aFileIdsIndex, + aFileManager, + aInfo); + } + + static nsresult + BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, + mozIStorageStatement* aStatement); + + static nsresult + BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, + mozIStorageStatement* aStatement, + const nsCString& aLocale); + + static void + AppendConditionClause(const nsACString& aColumnName, + const nsACString& aArgName, + bool aLessThan, + bool aEquals, + nsAutoCString& aResult); + + static nsresult + GetUniqueIndexTableForObjectStore( + TransactionBase* aTransaction, + int64_t aObjectStoreId, + Maybe<UniqueIndexTable>& aMaybeUniqueIndexTable); + + static nsresult + IndexDataValuesFromUpdateInfos(const nsTArray<IndexUpdateInfo>& aUpdateInfos, + const UniqueIndexTable& aUniqueIndexTable, + nsTArray<IndexDataValue>& aIndexValues); + + static nsresult + InsertIndexTableRows(DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const Key& aObjectStoreKey, + const FallibleTArray<IndexDataValue>& aIndexValues); + + static nsresult + DeleteIndexDataTableRows(DatabaseConnection* aConnection, + const Key& aObjectStoreKey, + const FallibleTArray<IndexDataValue>& aIndexValues); + + static nsresult + DeleteObjectStoreDataTableRowsWithIndexes(DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const OptionalKeyRange& aKeyRange); + + static nsresult + UpdateIndexValues(DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const Key& aObjectStoreKey, + const FallibleTArray<IndexDataValue>& aIndexValues); + + static nsresult + ObjectStoreHasIndexes(DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + bool* aHasIndexes); + +private: + template <typename T> + static nsresult + GetStructuredCloneReadInfoFromSource(T* aSource, + uint32_t aDataIndex, + uint32_t aFileIdsIndex, + FileManager* aFileManager, + StructuredCloneReadInfo* aInfo); + + static nsresult + GetStructuredCloneReadInfoFromBlob(const uint8_t* aBlobData, + uint32_t aBlobDataLength, + FileManager* aFileManager, + const nsAString& aFileIds, + StructuredCloneReadInfo* aInfo); + + static nsresult + GetStructuredCloneReadInfoFromExternalBlob(uint64_t aIntData, + FileManager* aFileManager, + const nsAString& aFileIds, + StructuredCloneReadInfo* aInfo); + + // Not to be overridden by subclasses. + NS_DECL_MOZISTORAGEPROGRESSHANDLER +}; + +class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final +{ + mozIStorageConnection* mConnection; +#ifdef DEBUG + DatabaseOperationBase* mDEBUGDatabaseOp; +#endif + +public: + AutoSetProgressHandler(); + + ~AutoSetProgressHandler(); + + nsresult + Register(mozIStorageConnection* aConnection, + DatabaseOperationBase* aDatabaseOp); +}; + +class TransactionDatabaseOperationBase + : public DatabaseOperationBase +{ + enum class InternalState + { + Initial, + DatabaseWork, + SendingPreprocess, + WaitingForContinue, + SendingResults, + Completed + }; + + RefPtr<TransactionBase> mTransaction; + const int64_t mTransactionLoggingSerialNumber; + InternalState mInternalState; + const bool mTransactionIsAborted; + +public: + void + AssertIsOnConnectionThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + uint64_t + StartOnConnectionPool(const nsID& aBackgroundChildLoggingId, + const nsACString& aDatabaseId, + int64_t aLoggingSerialNumber, + const nsTArray<nsString>& aObjectStoreNames, + bool aIsWriteTransaction); + + void + DispatchToConnectionPool(); + + TransactionBase* + Transaction() const + { + MOZ_ASSERT(mTransaction); + + return mTransaction; + } + + void + NoteContinueReceived(); + + // May be overridden by subclasses if they need to perform work on the + // background thread before being dispatched. Returning false will kill the + // child actors and prevent dispatch. + virtual bool + Init(TransactionBase* aTransaction); + + // This callback will be called on the background thread before releasing the + // final reference to this request object. Subclasses may perform any + // additional cleanup here but must always call the base class implementation. + virtual void + Cleanup(); + +protected: + explicit + TransactionDatabaseOperationBase(TransactionBase* aTransaction); + + TransactionDatabaseOperationBase(TransactionBase* aTransaction, + uint64_t aLoggingSerialNumber); + + virtual + ~TransactionDatabaseOperationBase(); + + virtual void + RunOnConnectionThread(); + + // Must be overridden in subclasses. Called on the target thread to allow the + // subclass to perform necessary database or file operations. A successful + // return value will trigger a SendSuccessResult callback on the background + // thread while a failure value will trigger a SendFailureResult callback. + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) = 0; + + // May be overriden in subclasses. Called on the background thread to decide + // if the subclass needs to send any preprocess info to the child actor. + virtual bool + HasPreprocessInfo(); + + // May be overriden in subclasses. Called on the background thread to allow + // the subclass to serialize its preprocess info and send it to the child + // actor. A successful return value will trigger a wait for a + // NoteContinueReceived callback on the background thread while a failure + // value will trigger a SendFailureResult callback. + virtual nsresult + SendPreprocessInfo(); + + // Must be overridden in subclasses. Called on the background thread to allow + // the subclass to serialize its results and send them to the child actor. A + // failed return value will trigger a SendFailureResult callback. + virtual nsresult + SendSuccessResult() = 0; + + // Must be overridden in subclasses. Called on the background thread to allow + // the subclass to send its failure code. Returning false will cause the + // transaction to be aborted with aResultCode. Returning true will not cause + // the transaction to be aborted. + virtual bool + SendFailureResult(nsresult aResultCode) = 0; + +private: + void + SendToConnectionPool(); + + void + SendPreprocess(); + + void + SendResults(); + + void + SendPreprocessInfoOrResults(bool aSendPreprocessInfo); + + // Not to be overridden by subclasses. + NS_DECL_NSIRUNNABLE +}; + +class Factory final + : public PBackgroundIDBFactoryParent +{ + + RefPtr<DatabaseLoggingInfo> mLoggingInfo; + +#ifdef DEBUG + bool mActorDestroyed; +#endif + +public: + static already_AddRefed<Factory> + Create(const LoggingInfo& aLoggingInfo); + + DatabaseLoggingInfo* + GetLoggingInfo() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mLoggingInfo); + + return mLoggingInfo; + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Factory) + +private: + // Only constructed in Create(). + explicit + Factory(already_AddRefed<DatabaseLoggingInfo> aLoggingInfo); + + // Reference counted. + ~Factory(); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvDeleteMe() override; + + virtual bool + RecvIncrementLoggingRequestSerialNumber() override; + + virtual PBackgroundIDBFactoryRequestParent* + AllocPBackgroundIDBFactoryRequestParent(const FactoryRequestParams& aParams) + override; + + virtual bool + RecvPBackgroundIDBFactoryRequestConstructor( + PBackgroundIDBFactoryRequestParent* aActor, + const FactoryRequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBFactoryRequestParent( + PBackgroundIDBFactoryRequestParent* aActor) + override; + + virtual PBackgroundIDBDatabaseParent* + AllocPBackgroundIDBDatabaseParent( + const DatabaseSpec& aSpec, + PBackgroundIDBFactoryRequestParent* aRequest) + override; + + virtual bool + DeallocPBackgroundIDBDatabaseParent(PBackgroundIDBDatabaseParent* aActor) + override; +}; + +class WaitForTransactionsHelper final + : public Runnable +{ + const nsCString mDatabaseId; + nsCOMPtr<nsIRunnable> mCallback; + + enum class State + { + Initial = 0, + WaitingForTransactions, + WaitingForFileHandles, + Complete + } mState; + +public: + WaitForTransactionsHelper(const nsCString& aDatabaseId, + nsIRunnable* aCallback) + : mDatabaseId(aDatabaseId) + , mCallback(aCallback) + , mState(State::Initial) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!aDatabaseId.IsEmpty()); + MOZ_ASSERT(aCallback); + } + + void + WaitForTransactions(); + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~WaitForTransactionsHelper() + { + MOZ_ASSERT(!mCallback); + MOZ_ASSERT(mState == State::Complete); + } + + void + MaybeWaitForTransactions(); + + void + MaybeWaitForFileHandles(); + + void + CallCallback(); + + NS_DECL_NSIRUNNABLE +}; + +class Database final + : public PBackgroundIDBDatabaseParent +{ + friend class VersionChangeTransaction; + + class StartTransactionOp; + +private: + RefPtr<Factory> mFactory; + RefPtr<FullDatabaseMetadata> mMetadata; + RefPtr<FileManager> mFileManager; + RefPtr<DirectoryLock> mDirectoryLock; + nsTHashtable<nsPtrHashKey<TransactionBase>> mTransactions; + nsTHashtable<nsPtrHashKey<MutableFile>> mMutableFiles; + RefPtr<DatabaseConnection> mConnection; + const PrincipalInfo mPrincipalInfo; + const Maybe<ContentParentId> mOptionalContentParentId; + const nsCString mGroup; + const nsCString mOrigin; + const nsCString mId; + const nsString mFilePath; + uint32_t mActiveMutableFileCount; + const uint32_t mTelemetryId; + const PersistenceType mPersistenceType; + const bool mFileHandleDisabled; + const bool mChromeWriteAccessAllowed; + bool mClosed; + bool mInvalidated; + bool mActorWasAlive; + bool mActorDestroyed; + bool mMetadataCleanedUp; + +public: + // Created by OpenDatabaseOp. + Database(Factory* aFactory, + const PrincipalInfo& aPrincipalInfo, + const Maybe<ContentParentId>& aOptionalContentParentId, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId, + FullDatabaseMetadata* aMetadata, + FileManager* aFileManager, + already_AddRefed<DirectoryLock> aDirectoryLock, + bool aFileHandleDisabled, + bool aChromeWriteAccessAllowed); + + void + AssertIsOnConnectionThread() const + { +#ifdef DEBUG + if (mConnection) { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + } else { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mInvalidated); + } +#endif + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Database) + + void + Invalidate(); + + const PrincipalInfo& + GetPrincipalInfo() const + { + return mPrincipalInfo; + } + + bool + IsOwnedByProcess(ContentParentId aContentParentId) const + { + return + mOptionalContentParentId && + mOptionalContentParentId.value() == aContentParentId; + } + + const nsCString& + Group() const + { + return mGroup; + } + + const nsCString& + Origin() const + { + return mOrigin; + } + + const nsCString& + Id() const + { + return mId; + } + + uint32_t + TelemetryId() const + { + return mTelemetryId; + } + + PersistenceType + Type() const + { + return mPersistenceType; + } + + const nsString& + FilePath() const + { + return mFilePath; + } + + FileManager* + GetFileManager() const + { + return mFileManager; + } + + FullDatabaseMetadata* + Metadata() const + { + MOZ_ASSERT(mMetadata); + return mMetadata; + } + + PBackgroundParent* + GetBackgroundParent() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return Manager()->Manager(); + } + + DatabaseLoggingInfo* + GetLoggingInfo() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFactory); + + return mFactory->GetLoggingInfo(); + } + + void + ReleaseTransactionThreadObjects(); + + void + ReleaseBackgroundThreadObjects(); + + bool + RegisterTransaction(TransactionBase* aTransaction); + + void + UnregisterTransaction(TransactionBase* aTransaction); + + bool + IsFileHandleDisabled() const + { + return mFileHandleDisabled; + } + + bool + RegisterMutableFile(MutableFile* aMutableFile); + + void + UnregisterMutableFile(MutableFile* aMutableFile); + + void + NoteActiveMutableFile(); + + void + NoteInactiveMutableFile(); + + void + SetActorAlive(); + + bool + IsActorAlive() const + { + AssertIsOnBackgroundThread(); + + return mActorWasAlive && !mActorDestroyed; + } + + bool + IsActorDestroyed() const + { + AssertIsOnBackgroundThread(); + + return mActorWasAlive && mActorDestroyed; + } + + bool + IsClosed() const + { + AssertIsOnBackgroundThread(); + + return mClosed; + } + + bool + IsInvalidated() const + { + AssertIsOnBackgroundThread(); + + return mInvalidated; + } + + nsresult + EnsureConnection(); + + DatabaseConnection* + GetConnection() const + { +#ifdef DEBUG + if (mConnection) { + mConnection->AssertIsOnConnectionThread(); + } +#endif + + return mConnection; + } + +private: + // Reference counted. + ~Database() + { + MOZ_ASSERT(mClosed); + MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed); + } + + bool + CloseInternal(); + + void + MaybeCloseConnection(); + + void + ConnectionClosedCallback(); + + void + CleanupMetadata(); + + bool + VerifyRequestParams(const DatabaseRequestParams& aParams) const; + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PBackgroundIDBDatabaseFileParent* + AllocPBackgroundIDBDatabaseFileParent(PBlobParent* aBlobParent) + override; + + virtual bool + DeallocPBackgroundIDBDatabaseFileParent( + PBackgroundIDBDatabaseFileParent* aActor) + override; + + virtual PBackgroundIDBDatabaseRequestParent* + AllocPBackgroundIDBDatabaseRequestParent(const DatabaseRequestParams& aParams) + override; + + virtual bool + RecvPBackgroundIDBDatabaseRequestConstructor( + PBackgroundIDBDatabaseRequestParent* aActor, + const DatabaseRequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBDatabaseRequestParent( + PBackgroundIDBDatabaseRequestParent* aActor) + override; + + virtual PBackgroundIDBTransactionParent* + AllocPBackgroundIDBTransactionParent( + const nsTArray<nsString>& aObjectStoreNames, + const Mode& aMode) + override; + + virtual bool + RecvPBackgroundIDBTransactionConstructor( + PBackgroundIDBTransactionParent* aActor, + InfallibleTArray<nsString>&& aObjectStoreNames, + const Mode& aMode) + override; + + virtual bool + DeallocPBackgroundIDBTransactionParent( + PBackgroundIDBTransactionParent* aActor) + override; + + virtual PBackgroundIDBVersionChangeTransactionParent* + AllocPBackgroundIDBVersionChangeTransactionParent( + const uint64_t& aCurrentVersion, + const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, + const int64_t& aNextIndexId) + override; + + virtual bool + DeallocPBackgroundIDBVersionChangeTransactionParent( + PBackgroundIDBVersionChangeTransactionParent* aActor) + override; + + virtual PBackgroundMutableFileParent* + AllocPBackgroundMutableFileParent(const nsString& aName, + const nsString& aType) override; + + virtual bool + DeallocPBackgroundMutableFileParent(PBackgroundMutableFileParent* aActor) + override; + + virtual bool + RecvDeleteMe() override; + + virtual bool + RecvBlocked() override; + + virtual bool + RecvClose() override; +}; + +class Database::StartTransactionOp final + : public TransactionDatabaseOperationBase +{ + friend class Database; + +private: + explicit + StartTransactionOp(TransactionBase* aTransaction) + : TransactionDatabaseOperationBase(aTransaction, + /* aLoggingSerialNumber */ 0) + { } + + ~StartTransactionOp() + { } + + virtual void + RunOnConnectionThread() override; + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual nsresult + SendSuccessResult() override; + + virtual bool + SendFailureResult(nsresult aResultCode) override; + + virtual void + Cleanup() override; +}; + +/** + * In coordination with IDBDatabase's mFileActors weak-map on the child side, a + * long-lived mapping from a child process's live Blobs to their corresponding + * FileInfo in our owning database. Assists in avoiding redundant IPC traffic + * and disk storage. This includes both: + * - Blobs retrieved from this database and sent to the child that do not need + * to be written to disk because they already exist on disk in this database's + * files directory. + * - Blobs retrieved from other databases (that are therefore !IsShareable()) + * or from anywhere else that will need to be written to this database's files + * directory. In this case we will hold a reference to its BlobImpl in + * mBlobImpl until we have successfully written the Blob to disk. + * + * Relevant Blob context: Blobs sent from the parent process to child processes + * are automatically linked back to their source BlobImpl when the child process + * references the Blob via IPC. (This is true even when a new "KnownBlob" actor + * must be created because the reference is occurring on a different thread than + * the PBlob actor created when the blob was sent to the child.) However, when + * getting an actor in the child process for sending an in-child-created Blob to + * the parent process, there is (currently) no Blob machinery to automatically + * establish and reuse a long-lived Actor. As a result, without IDB's weak-map + * cleverness, a memory-backed Blob repeatedly sent from the child to the parent + * would appear as a different Blob each time, requiring the Blob data to be + * sent over IPC each time as well as potentially needing to be written to disk + * each time. + * + * This object remains alive as long as there is an active child actor or an + * ObjectStoreAddOrPutRequestOp::StoredFileInfo for a queued or active add/put + * op is holding a reference to us. + */ +class DatabaseFile final + : public PBackgroundIDBDatabaseFileParent +{ + friend class Database; + + // mBlobImpl's ownership lifecycle: + // - Initialized on the background thread at creation time. Then + // responsibility is handed off to the connection thread. + // - Checked and used by the connection thread to generate a stream to write + // the blob to disk by an add/put operation. + // - Cleared on the connection thread once the file has successfully been + // written to disk. + RefPtr<BlobImpl> mBlobImpl; + RefPtr<FileInfo> mFileInfo; + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::DatabaseFile); + + FileInfo* + GetFileInfo() const + { + AssertIsOnBackgroundThread(); + + return mFileInfo; + } + + /** + * If mBlobImpl is non-null (implying the contents of this file have not yet + * been written to disk), then return an input stream that is guaranteed to + * block on reads and never return NS_BASE_STREAM_WOULD_BLOCK. If mBlobImpl + * is null (because the contents have been written to disk), returns null. + * + * Because this method does I/O, it should only be called on a database I/O + * thread, not on PBackground. Note that we actually open the stream on this + * thread, where previously it was opened on PBackground. This is safe and + * equally efficient because blob implementations are thread-safe and blobs in + * the parent are fully populated. (This would not be efficient in the child + * where the open request would have to be dispatched back to the PBackground + * thread because of the need to potentially interact with actors.) + * + * We enforce this guarantee by wrapping the stream in a blocking pipe unless + * either is true: + * - The stream is already a blocking stream. (AKA it's not non-blocking.) + * This is the case for nsFileStreamBase-derived implementations and + * appropriately configured nsPipes (but not PSendStream-generated pipes). + * - The stream already contains the entire contents of the Blob. For + * example, nsStringInputStreams report as non-blocking, but also have their + * entire contents synchronously available, so they will never return + * NS_BASE_STREAM_WOULD_BLOCK. There is no need to wrap these in a pipe. + * (It's also very common for SendStream-based Blobs to have their contents + * entirely streamed into the parent process by the time this call is + * issued.) + * + * This additional logic is necessary because our database operations all + * are written in such a way that the operation is assumed to have completed + * when they yield control-flow, and: + * - When memory-backed blobs cross a certain threshold (1MiB at the time of + * writing), they will be sent up from the child via PSendStream in chunks + * to a non-blocking pipe that will return NS_BASE_STREAM_WOULD_BLOCK. + * - Other Blob types could potentially be non-blocking. (We're not making + * any assumptions.) + */ + already_AddRefed<nsIInputStream> + GetBlockingInputStream(ErrorResult &rv) const; + + /** + * To be called upon successful copying of the stream GetBlockingInputStream() + * returned so that we won't try and redundantly write the file to disk in the + * future. This is a separate step from GetBlockingInputStream() because + * the write could fail due to quota errors that happen now but that might + * not happen in a future attempt. + */ + void + WriteSucceededClearBlobImpl() + { + MOZ_ASSERT(!IsOnBackgroundThread()); + + mBlobImpl = nullptr; + } + +private: + // Called when sending to the child. + explicit DatabaseFile(FileInfo* aFileInfo) + : mFileInfo(aFileInfo) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileInfo); + } + + // Called when receiving from the child. + DatabaseFile(BlobImpl* aBlobImpl, FileInfo* aFileInfo) + : mBlobImpl(aBlobImpl) + , mFileInfo(aFileInfo) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aFileInfo); + } + + ~DatabaseFile() + { } + + virtual void + ActorDestroy(ActorDestroyReason aWhy) override + { + AssertIsOnBackgroundThread(); + } +}; + +already_AddRefed<nsIInputStream> +DatabaseFile::GetBlockingInputStream(ErrorResult &rv) const +{ + // We should only be called from our DB connection thread, not the background + // thread. + MOZ_ASSERT(!IsOnBackgroundThread()); + + if (!mBlobImpl) { + return nullptr; + } + + nsCOMPtr<nsIInputStream> inputStream; + mBlobImpl->GetInternalStream(getter_AddRefs(inputStream), rv); + if (rv.Failed()) { + return nullptr; + } + + // If it's non-blocking we may need a pipe. + bool pipeNeeded; + rv = inputStream->IsNonBlocking(&pipeNeeded); + if (rv.Failed()) { + return nullptr; + } + + // We don't need a pipe if all the bytes might already be available. + if (pipeNeeded) { + uint64_t available; + rv = inputStream->Available(&available); + if (rv.Failed()) { + return nullptr; + } + + uint64_t blobSize = mBlobImpl->GetSize(rv); + if (rv.Failed()) { + return nullptr; + } + + if (available == blobSize) { + pipeNeeded = false; + } + } + + if (pipeNeeded) { + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + if (!target) { + rv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsCOMPtr<nsIInputStream> pipeInputStream; + nsCOMPtr<nsIOutputStream> pipeOutputStream; + + rv = NS_NewPipe( + getter_AddRefs(pipeInputStream), + getter_AddRefs(pipeOutputStream), + 0, 0, // default buffering is fine; + false, // we absolutely want a blocking input stream + true); // we don't need the writer to block + if (rv.Failed()) { + return nullptr; + } + + rv = NS_AsyncCopy(inputStream, pipeOutputStream, target); + if (rv.Failed()) { + return nullptr; + } + + inputStream = pipeInputStream; + } + + return inputStream.forget(); +} + + +class TransactionBase +{ + friend class Cursor; + + class CommitOp; + +protected: + typedef IDBTransaction::Mode Mode; + +private: + RefPtr<Database> mDatabase; + nsTArray<RefPtr<FullObjectStoreMetadata>> + mModifiedAutoIncrementObjectStoreMetadataArray; + uint64_t mTransactionId; + const nsCString mDatabaseId; + const int64_t mLoggingSerialNumber; + uint64_t mActiveRequestCount; + Atomic<bool> mInvalidatedOnAnyThread; + const Mode mMode; + bool mHasBeenActive; + bool mHasBeenActiveOnConnectionThread; + bool mActorDestroyed; + bool mInvalidated; + +protected: + nsresult mResultCode; + bool mCommitOrAbortReceived; + bool mCommittedOrAborted; + bool mForceAborted; + +public: + void + AssertIsOnConnectionThread() const + { + MOZ_ASSERT(mDatabase); + mDatabase->AssertIsOnConnectionThread(); + } + + bool + IsActorDestroyed() const + { + AssertIsOnBackgroundThread(); + + return mActorDestroyed; + } + + // Must be called on the background thread. + bool + IsInvalidated() const + { + MOZ_ASSERT(IsOnBackgroundThread(), "Use IsInvalidatedOnAnyThread()"); + MOZ_ASSERT_IF(mInvalidated, NS_FAILED(mResultCode)); + + return mInvalidated; + } + + // May be called on any thread, but is more expensive than IsInvalidated(). + bool + IsInvalidatedOnAnyThread() const + { + return mInvalidatedOnAnyThread; + } + + void + SetActive(uint64_t aTransactionId) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransactionId); + + mTransactionId = aTransactionId; + mHasBeenActive = true; + } + + void + SetActiveOnConnectionThread() + { + AssertIsOnConnectionThread(); + mHasBeenActiveOnConnectionThread = true; + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING( + mozilla::dom::indexedDB::TransactionBase) + + void + Abort(nsresult aResultCode, bool aForce); + + uint64_t + TransactionId() const + { + return mTransactionId; + } + + const nsCString& + DatabaseId() const + { + return mDatabaseId; + } + + Mode + GetMode() const + { + return mMode; + } + + Database* + GetDatabase() const + { + MOZ_ASSERT(mDatabase); + + return mDatabase; + } + + DatabaseLoggingInfo* + GetLoggingInfo() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabase); + + return mDatabase->GetLoggingInfo(); + } + + int64_t + LoggingSerialNumber() const + { + return mLoggingSerialNumber; + } + + bool + IsAborted() const + { + AssertIsOnBackgroundThread(); + + return NS_FAILED(mResultCode); + } + + already_AddRefed<FullObjectStoreMetadata> + GetMetadataForObjectStoreId(int64_t aObjectStoreId) const; + + already_AddRefed<FullIndexMetadata> + GetMetadataForIndexId(FullObjectStoreMetadata* const aObjectStoreMetadata, + int64_t aIndexId) const; + + PBackgroundParent* + GetBackgroundParent() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return GetDatabase()->GetBackgroundParent(); + } + + void + NoteModifiedAutoIncrementObjectStore(FullObjectStoreMetadata* aMetadata); + + void + ForgetModifiedAutoIncrementObjectStore(FullObjectStoreMetadata* aMetadata); + + void + NoteActiveRequest(); + + void + NoteFinishedRequest(); + + void + Invalidate(); + +protected: + TransactionBase(Database* aDatabase, Mode aMode); + + virtual + ~TransactionBase(); + + void + NoteActorDestroyed() + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + } + +#ifdef DEBUG + // Only called by VersionChangeTransaction. + void + FakeActorDestroyed() + { + mActorDestroyed = true; + } +#endif + + bool + RecvCommit(); + + bool + RecvAbort(nsresult aResultCode); + + void + MaybeCommitOrAbort() + { + AssertIsOnBackgroundThread(); + + // If we've already committed or aborted then there's nothing else to do. + if (mCommittedOrAborted) { + return; + } + + // If there are active requests then we have to wait for those requests to + // complete (see NoteFinishedRequest). + if (mActiveRequestCount) { + return; + } + + // If we haven't yet received a commit or abort message then there could be + // additional requests coming so we should wait unless we're being forced to + // abort. + if (!mCommitOrAbortReceived && !mForceAborted) { + return; + } + + CommitOrAbort(); + } + + PBackgroundIDBRequestParent* + AllocRequest(const RequestParams& aParams, bool aTrustParams); + + bool + StartRequest(PBackgroundIDBRequestParent* aActor); + + bool + DeallocRequest(PBackgroundIDBRequestParent* aActor); + + PBackgroundIDBCursorParent* + AllocCursor(const OpenCursorParams& aParams, bool aTrustParams); + + bool + StartCursor(PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams); + + bool + DeallocCursor(PBackgroundIDBCursorParent* aActor); + + virtual void + UpdateMetadata(nsresult aResult) + { } + + virtual void + SendCompleteNotification(nsresult aResult) = 0; + +private: + bool + VerifyRequestParams(const RequestParams& aParams) const; + + bool + VerifyRequestParams(const SerializedKeyRange& aKeyRange) const; + + bool + VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const; + + bool + VerifyRequestParams(const OptionalKeyRange& aKeyRange) const; + + void + CommitOrAbort(); +}; + +class TransactionBase::CommitOp final + : public DatabaseOperationBase + , public ConnectionPool::FinishCallback +{ + friend class TransactionBase; + + RefPtr<TransactionBase> mTransaction; + nsresult mResultCode; + +private: + CommitOp(TransactionBase* aTransaction, nsresult aResultCode); + + ~CommitOp() + { } + + // Writes new autoIncrement counts to database. + nsresult + WriteAutoIncrementCounts(); + + // Updates counts after a database activity has finished. + void + CommitOrRollbackAutoIncrementCounts(); + + void + AssertForeignKeyConsistency(DatabaseConnection* aConnection) +#ifdef DEBUG + ; +#else + { } +#endif + + NS_DECL_NSIRUNNABLE + + virtual void + TransactionFinishedBeforeUnblock() override; + + virtual void + TransactionFinishedAfterUnblock() override; + +public: + NS_DECL_ISUPPORTS_INHERITED +}; + +class NormalTransaction final + : public TransactionBase + , public PBackgroundIDBTransactionParent +{ + friend class Database; + + nsTArray<RefPtr<FullObjectStoreMetadata>> mObjectStores; + +private: + // This constructor is only called by Database. + NormalTransaction(Database* aDatabase, + TransactionBase::Mode aMode, + nsTArray<RefPtr<FullObjectStoreMetadata>>& aObjectStores); + + // Reference counted. + ~NormalTransaction() + { } + + bool + IsSameProcessActor(); + + // Only called by TransactionBase. + virtual void + SendCompleteNotification(nsresult aResult) override; + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvDeleteMe() override; + + virtual bool + RecvCommit() override; + + virtual bool + RecvAbort(const nsresult& aResultCode) override; + + virtual PBackgroundIDBRequestParent* + AllocPBackgroundIDBRequestParent(const RequestParams& aParams) override; + + virtual bool + RecvPBackgroundIDBRequestConstructor(PBackgroundIDBRequestParent* aActor, + const RequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBRequestParent(PBackgroundIDBRequestParent* aActor) + override; + + virtual PBackgroundIDBCursorParent* + AllocPBackgroundIDBCursorParent(const OpenCursorParams& aParams) override; + + virtual bool + RecvPBackgroundIDBCursorConstructor(PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBCursorParent(PBackgroundIDBCursorParent* aActor) + override; +}; + +class VersionChangeTransaction final + : public TransactionBase + , public PBackgroundIDBVersionChangeTransactionParent +{ + friend class OpenDatabaseOp; + + RefPtr<OpenDatabaseOp> mOpenDatabaseOp; + RefPtr<FullDatabaseMetadata> mOldMetadata; + + bool mActorWasAlive; + +private: + // Only called by OpenDatabaseOp. + explicit VersionChangeTransaction(OpenDatabaseOp* aOpenDatabaseOp); + + // Reference counted. + ~VersionChangeTransaction(); + + bool + IsSameProcessActor(); + + // Only called by OpenDatabaseOp. + bool + CopyDatabaseMetadata(); + + void + SetActorAlive(); + + // Only called by TransactionBase. + virtual void + UpdateMetadata(nsresult aResult) override; + + // Only called by TransactionBase. + virtual void + SendCompleteNotification(nsresult aResult) override; + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvDeleteMe() override; + + virtual bool + RecvCommit() override; + + virtual bool + RecvAbort(const nsresult& aResultCode) override; + + virtual bool + RecvCreateObjectStore(const ObjectStoreMetadata& aMetadata) override; + + virtual bool + RecvDeleteObjectStore(const int64_t& aObjectStoreId) override; + + virtual bool + RecvRenameObjectStore(const int64_t& aObjectStoreId, + const nsString& aName) override; + + virtual bool + RecvCreateIndex(const int64_t& aObjectStoreId, + const IndexMetadata& aMetadata) override; + + virtual bool + RecvDeleteIndex(const int64_t& aObjectStoreId, + const int64_t& aIndexId) override; + + virtual bool + RecvRenameIndex(const int64_t& aObjectStoreId, + const int64_t& aIndexId, + const nsString& aName) override; + + virtual PBackgroundIDBRequestParent* + AllocPBackgroundIDBRequestParent(const RequestParams& aParams) override; + + virtual bool + RecvPBackgroundIDBRequestConstructor(PBackgroundIDBRequestParent* aActor, + const RequestParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBRequestParent(PBackgroundIDBRequestParent* aActor) + override; + + virtual PBackgroundIDBCursorParent* + AllocPBackgroundIDBCursorParent(const OpenCursorParams& aParams) override; + + virtual bool + RecvPBackgroundIDBCursorConstructor(PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) + override; + + virtual bool + DeallocPBackgroundIDBCursorParent(PBackgroundIDBCursorParent* aActor) + override; +}; + +class MutableFile + : public BackgroundMutableFileParentBase +{ + RefPtr<Database> mDatabase; + RefPtr<FileInfo> mFileInfo; + +public: + static already_AddRefed<MutableFile> + Create(nsIFile* aFile, + Database* aDatabase, + FileInfo* aFileInfo); + + Database* + GetDatabase() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabase); + + return mDatabase; + } + + FileInfo* + GetFileInfo() const + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFileInfo); + + return mFileInfo; + } + + virtual void + NoteActiveState() override; + + virtual void + NoteInactiveState() override; + + virtual PBackgroundParent* + GetBackgroundParent() const override; + + virtual already_AddRefed<nsISupports> + CreateStream(bool aReadOnly) override; + + virtual already_AddRefed<BlobImpl> + CreateBlobImpl() override; + +private: + MutableFile(nsIFile* aFile, + Database* aDatabase, + FileInfo* aFileInfo); + + ~MutableFile(); + + virtual PBackgroundFileHandleParent* + AllocPBackgroundFileHandleParent(const FileMode& aMode) override; + + virtual bool + RecvPBackgroundFileHandleConstructor(PBackgroundFileHandleParent* aActor, + const FileMode& aMode) override; + + virtual bool + RecvGetFileId(int64_t* aFileId) override; +}; + +class FactoryOp + : public DatabaseOperationBase + , public OpenDirectoryListener + , public PBackgroundIDBFactoryRequestParent +{ +public: + struct MaybeBlockedDatabaseInfo; + +protected: + enum class State + { + // Just created on the PBackground thread, dispatched to the main thread. + // Next step is either SendingResults if permission is denied, + // PermissionChallenge if the permission is unknown, or FinishOpen + // if permission is granted. + Initial, + + // Sending a permission challenge message to the child on the PBackground + // thread. Next step is PermissionRetry. + PermissionChallenge, + + // Retrying permission check after a challenge on the main thread. Next step + // is either SendingResults if permission is denied or FinishOpen + // if permission is granted. + PermissionRetry, + + // Opening directory or initializing quota manager on the PBackground + // thread. Next step is either DirectoryOpenPending if quota manager is + // already initialized or QuotaManagerPending if quota manager needs to be + // initialized. + FinishOpen, + + // Waiting for quota manager initialization to complete on the PBackground + // thread. Next step is either SendingResults if initialization failed or + // DirectoryOpenPending if initialization succeeded. + QuotaManagerPending, + + // Waiting for directory open allowed on the PBackground thread. The next + // step is either SendingResults if directory lock failed to acquire, or + // DatabaseOpenPending if directory lock is acquired. + DirectoryOpenPending, + + // Waiting for database open allowed on the PBackground thread. The next + // step is DatabaseWorkOpen. + DatabaseOpenPending, + + // Waiting to do/doing work on the QuotaManager IO thread. Its next step is + // either BeginVersionChange if the requested version doesn't match the + // existing database version or SendingResults if the versions match. + DatabaseWorkOpen, + + // Starting a version change transaction or deleting a database on the + // PBackground thread. We need to notify other databases that a version + // change is about to happen, and maybe tell the request that a version + // change has been blocked. If databases are notified then the next step is + // WaitingForOtherDatabasesToClose. Otherwise the next step is + // WaitingForTransactionsToComplete. + BeginVersionChange, + + // Waiting for other databases to close on the PBackground thread. This + // state may persist until all databases are closed. The next state is + // WaitingForTransactionsToComplete. + WaitingForOtherDatabasesToClose, + + // Waiting for all transactions that could interfere with this operation to + // complete on the PBackground thread. Next state is + // DatabaseWorkVersionChange. + WaitingForTransactionsToComplete, + + // Waiting to do/doing work on the "work thread". This involves waiting for + // the VersionChangeOp (OpenDatabaseOp and DeleteDatabaseOp each have a + // different implementation) to do its work. Eventually the state will + // transition to SendingResults. + DatabaseWorkVersionChange, + + // Waiting to send/sending results on the PBackground thread. Next step is + // Completed. + SendingResults, + + // All done. + Completed + }; + + // Must be released on the background thread! + RefPtr<Factory> mFactory; + + // Must be released on the main thread! + RefPtr<ContentParent> mContentParent; + + // Must be released on the main thread! + RefPtr<DirectoryLock> mDirectoryLock; + + RefPtr<FactoryOp> mDelayedOp; + nsTArray<MaybeBlockedDatabaseInfo> mMaybeBlockedDatabases; + + const CommonFactoryRequestParams mCommonParams; + nsCString mSuffix; + nsCString mGroup; + nsCString mOrigin; + nsCString mDatabaseId; + nsString mDatabaseFilePath; + State mState; + bool mIsApp; + bool mEnforcingQuota; + const bool mDeleting; + bool mBlockedDatabaseOpen; + bool mChromeWriteAccessAllowed; + bool mFileHandleDisabled; + +public: + void + NoteDatabaseBlocked(Database* aDatabase); + + virtual void + NoteDatabaseClosed(Database* aDatabase) = 0; + +#ifdef DEBUG + bool + HasBlockedDatabases() const + { + return !mMaybeBlockedDatabases.IsEmpty(); + } +#endif + + const nsString& + DatabaseFilePath() const + { + return mDatabaseFilePath; + } + +protected: + FactoryOp(Factory* aFactory, + already_AddRefed<ContentParent> aContentParent, + const CommonFactoryRequestParams& aCommonParams, + bool aDeleting); + + virtual + ~FactoryOp() + { + // Normally this would be out-of-line since it is a virtual function but + // MSVC 2010 fails to link for some reason if it is not inlined here... + MOZ_ASSERT_IF(OperationMayProceed(), + mState == State::Initial || mState == State::Completed); + } + + nsresult + Open(); + + nsresult + ChallengePermission(); + + nsresult + RetryCheckPermission(); + + nsresult + DirectoryOpen(); + + nsresult + SendToIOThread(); + + void + WaitForTransactions(); + + void + FinishSendResults(); + + nsresult + SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo, + Database* aOpeningDatabase, + uint64_t aOldVersion, + const NullableVersion& aNewVersion); + + // Methods that subclasses must implement. + virtual nsresult + DatabaseOpen() = 0; + + virtual nsresult + DoDatabaseWork() = 0; + + virtual nsresult + BeginVersionChange() = 0; + + virtual nsresult + DispatchToWorkThread() = 0; + + // Should only be called by Run(). + virtual void + SendResults() = 0; + + NS_DECL_ISUPPORTS_INHERITED + + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // OpenDirectoryListener overrides. + virtual void + DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void + DirectoryLockFailed() override; + + // IPDL methods. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvPermissionRetry() override; + + virtual void + SendBlockedNotification() = 0; + +private: + nsresult + CheckPermission(ContentParent* aContentParent, + PermissionRequestBase::PermissionValue* aPermission); + + static bool + CheckAtLeastOneAppHasPermission(ContentParent* aContentParent, + const nsACString& aPermissionString); + + nsresult + FinishOpen(); + + nsresult + QuotaManagerOpen(); + + nsresult + OpenDirectory(); + + // Test whether this FactoryOp needs to wait for the given op. + bool + MustWaitFor(const FactoryOp& aExistingOp); +}; + +struct FactoryOp::MaybeBlockedDatabaseInfo final +{ + RefPtr<Database> mDatabase; + bool mBlocked; + + MOZ_IMPLICIT MaybeBlockedDatabaseInfo(Database* aDatabase) + : mDatabase(aDatabase) + , mBlocked(false) + { + MOZ_ASSERT(aDatabase); + + MOZ_COUNT_CTOR(FactoryOp::MaybeBlockedDatabaseInfo); + } + + ~MaybeBlockedDatabaseInfo() + { + MOZ_COUNT_DTOR(FactoryOp::MaybeBlockedDatabaseInfo); + } + + bool + operator==(const MaybeBlockedDatabaseInfo& aOther) const + { + return mDatabase == aOther.mDatabase; + } + + bool + operator<(const MaybeBlockedDatabaseInfo& aOther) const + { + return mDatabase < aOther.mDatabase; + } + + Database* + operator->() MOZ_NO_ADDREF_RELEASE_ON_RETURN + { + return mDatabase; + } +}; + +class OpenDatabaseOp final + : public FactoryOp +{ + friend class Database; + friend class VersionChangeTransaction; + + class VersionChangeOp; + + Maybe<ContentParentId> mOptionalContentParentId; + + RefPtr<FullDatabaseMetadata> mMetadata; + + uint64_t mRequestedVersion; + RefPtr<FileManager> mFileManager; + + RefPtr<Database> mDatabase; + RefPtr<VersionChangeTransaction> mVersionChangeTransaction; + + // This is only set while a VersionChangeOp is live. It holds a strong + // reference to its OpenDatabaseOp object so this is a weak pointer to avoid + // cycles. + VersionChangeOp* mVersionChangeOp; + + uint32_t mTelemetryId; + +public: + OpenDatabaseOp(Factory* aFactory, + already_AddRefed<ContentParent> aContentParent, + const CommonFactoryRequestParams& aParams); + + bool + IsOtherProcessActor() const + { + return mOptionalContentParentId.isSome(); + } + +private: + ~OpenDatabaseOp() + { + MOZ_ASSERT(!mVersionChangeOp); + } + + nsresult + LoadDatabaseInformation(mozIStorageConnection* aConnection); + + nsresult + SendUpgradeNeeded(); + + void + EnsureDatabaseActor(); + + nsresult + EnsureDatabaseActorIsAlive(); + + void + MetadataToSpec(DatabaseSpec& aSpec); + + void + AssertMetadataConsistency(const FullDatabaseMetadata* aMetadata) +#ifdef DEBUG + ; +#else + { } +#endif + + void + ConnectionClosedCallback(); + + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual nsresult + DatabaseOpen() override; + + virtual nsresult + DoDatabaseWork() override; + + virtual nsresult + BeginVersionChange() override; + + virtual void + NoteDatabaseClosed(Database* aDatabase) override; + + virtual void + SendBlockedNotification() override; + + virtual nsresult + DispatchToWorkThread() override; + + virtual void + SendResults() override; + +#ifdef ENABLE_INTL_API + static nsresult + UpdateLocaleAwareIndex(mozIStorageConnection* aConnection, + const IndexMetadata& aIndexMetadata, + const nsCString& aLocale); +#endif +}; + +class OpenDatabaseOp::VersionChangeOp final + : public TransactionDatabaseOperationBase +{ + friend class OpenDatabaseOp; + + RefPtr<OpenDatabaseOp> mOpenDatabaseOp; + const uint64_t mRequestedVersion; + uint64_t mPreviousVersion; + +private: + explicit + VersionChangeOp(OpenDatabaseOp* aOpenDatabaseOp) + : TransactionDatabaseOperationBase( + aOpenDatabaseOp->mVersionChangeTransaction, + aOpenDatabaseOp->LoggingSerialNumber()) + , mOpenDatabaseOp(aOpenDatabaseOp) + , mRequestedVersion(aOpenDatabaseOp->mRequestedVersion) + , mPreviousVersion(aOpenDatabaseOp->mMetadata->mCommonMetadata.version()) + { + MOZ_ASSERT(aOpenDatabaseOp); + MOZ_ASSERT(mRequestedVersion); + } + + ~VersionChangeOp() + { + MOZ_ASSERT(!mOpenDatabaseOp); + } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual nsresult + SendSuccessResult() override; + + virtual bool + SendFailureResult(nsresult aResultCode) override; + + virtual void + Cleanup() override; +}; + +class DeleteDatabaseOp final + : public FactoryOp +{ + class VersionChangeOp; + + nsString mDatabaseDirectoryPath; + nsString mDatabaseFilenameBase; + uint64_t mPreviousVersion; + +public: + DeleteDatabaseOp(Factory* aFactory, + already_AddRefed<ContentParent> aContentParent, + const CommonFactoryRequestParams& aParams) + : FactoryOp(aFactory, Move(aContentParent), aParams, /* aDeleting */ true) + , mPreviousVersion(0) + { } + +private: + ~DeleteDatabaseOp() + { } + + void + LoadPreviousVersion(nsIFile* aDatabaseFile); + + virtual nsresult + DatabaseOpen() override; + + virtual nsresult + DoDatabaseWork() override; + + virtual nsresult + BeginVersionChange() override; + + virtual void + NoteDatabaseClosed(Database* aDatabase) override; + + virtual void + SendBlockedNotification() override; + + virtual nsresult + DispatchToWorkThread() override; + + virtual void + SendResults() override; +}; + +class DeleteDatabaseOp::VersionChangeOp final + : public DatabaseOperationBase +{ + friend class DeleteDatabaseOp; + + RefPtr<DeleteDatabaseOp> mDeleteDatabaseOp; + +private: + explicit + VersionChangeOp(DeleteDatabaseOp* aDeleteDatabaseOp) + : DatabaseOperationBase(aDeleteDatabaseOp->BackgroundChildLoggingId(), + aDeleteDatabaseOp->LoggingSerialNumber()) + , mDeleteDatabaseOp(aDeleteDatabaseOp) + { + MOZ_ASSERT(aDeleteDatabaseOp); + MOZ_ASSERT(!aDeleteDatabaseOp->mDatabaseDirectoryPath.IsEmpty()); + } + + ~VersionChangeOp() + { } + + nsresult + RunOnIOThread(); + + void + RunOnOwningThread(); + + nsresult + DeleteFile(nsIFile* aDirectory, + const nsAString& aFilename, + QuotaManager* aQuotaManager); + + NS_DECL_NSIRUNNABLE +}; + +class DatabaseOp + : public DatabaseOperationBase + , public PBackgroundIDBDatabaseRequestParent +{ +protected: + RefPtr<Database> mDatabase; + + enum class State + { + // Just created on the PBackground thread, dispatched to the main thread. + // Next step is DatabaseWork. + Initial, + + // Waiting to do/doing work on the QuotaManager IO thread. Next step is + // SendingResults. + DatabaseWork, + + // Waiting to send/sending results on the PBackground thread. Next step is + // Completed. + SendingResults, + + // All done. + Completed + }; + + State mState; + +public: + void + RunImmediately() + { + MOZ_ASSERT(mState == State::Initial); + + Unused << this->Run(); + } + +protected: + DatabaseOp(Database* aDatabase); + + virtual + ~DatabaseOp() + { + MOZ_ASSERT_IF(OperationMayProceed(), + mState == State::Initial || mState == State::Completed); + } + + nsresult + SendToIOThread(); + + // Methods that subclasses must implement. + virtual nsresult + DoDatabaseWork() = 0; + + virtual void + SendResults() = 0; + + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // IPDL methods. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class CreateFileOp final + : public DatabaseOp +{ + const CreateFileParams mParams; + + RefPtr<FileInfo> mFileInfo; + +public: + CreateFileOp(Database* aDatabase, + const DatabaseRequestParams& aParams); + +private: + ~CreateFileOp() + { } + + nsresult + CreateMutableFile(MutableFile** aMutableFile); + + virtual nsresult + DoDatabaseWork() override; + + virtual void + SendResults() override; +}; + +class VersionChangeTransactionOp + : public TransactionDatabaseOperationBase +{ +public: + virtual void + Cleanup() override; + +protected: + explicit VersionChangeTransactionOp(VersionChangeTransaction* aTransaction) + : TransactionDatabaseOperationBase(aTransaction) + { } + + virtual + ~VersionChangeTransactionOp() + { } + +private: + virtual nsresult + SendSuccessResult() override; + + virtual bool + SendFailureResult(nsresult aResultCode) override; +}; + +class CreateObjectStoreOp final + : public VersionChangeTransactionOp +{ + friend class VersionChangeTransaction; + + const ObjectStoreMetadata mMetadata; + +private: + // Only created by VersionChangeTransaction. + CreateObjectStoreOp(VersionChangeTransaction* aTransaction, + const ObjectStoreMetadata& aMetadata) + : VersionChangeTransactionOp(aTransaction) + , mMetadata(aMetadata) + { + MOZ_ASSERT(aMetadata.id()); + } + + ~CreateObjectStoreOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class DeleteObjectStoreOp final + : public VersionChangeTransactionOp +{ + friend class VersionChangeTransaction; + + const RefPtr<FullObjectStoreMetadata> mMetadata; + const bool mIsLastObjectStore; + +private: + // Only created by VersionChangeTransaction. + DeleteObjectStoreOp(VersionChangeTransaction* aTransaction, + FullObjectStoreMetadata* const aMetadata, + const bool aIsLastObjectStore) + : VersionChangeTransactionOp(aTransaction) + , mMetadata(aMetadata) + , mIsLastObjectStore(aIsLastObjectStore) + { + MOZ_ASSERT(aMetadata->mCommonMetadata.id()); + } + + ~DeleteObjectStoreOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class RenameObjectStoreOp final + : public VersionChangeTransactionOp +{ + friend class VersionChangeTransaction; + + const int64_t mId; + const nsString mNewName; + +private: + // Only created by VersionChangeTransaction. + RenameObjectStoreOp(VersionChangeTransaction* aTransaction, + FullObjectStoreMetadata* const aMetadata) + : VersionChangeTransactionOp(aTransaction) + , mId(aMetadata->mCommonMetadata.id()) + , mNewName(aMetadata->mCommonMetadata.name()) + { + MOZ_ASSERT(mId); + } + + ~RenameObjectStoreOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class CreateIndexOp final + : public VersionChangeTransactionOp +{ + friend class VersionChangeTransaction; + + class ThreadLocalJSContext; + class UpdateIndexDataValuesFunction; + + static const unsigned int kBadThreadLocalIndex = + static_cast<unsigned int>(-1); + + static unsigned int sThreadLocalIndex; + + const IndexMetadata mMetadata; + Maybe<UniqueIndexTable> mMaybeUniqueIndexTable; + RefPtr<FileManager> mFileManager; + const nsCString mDatabaseId; + const uint64_t mObjectStoreId; + +private: + // Only created by VersionChangeTransaction. + CreateIndexOp(VersionChangeTransaction* aTransaction, + const int64_t aObjectStoreId, + const IndexMetadata& aMetadata); + + ~CreateIndexOp() + { } + + nsresult + InsertDataFromObjectStore(DatabaseConnection* aConnection); + + nsresult + InsertDataFromObjectStoreInternal(DatabaseConnection* aConnection); + + virtual bool + Init(TransactionBase* aTransaction) override; + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class NormalJSContext +{ + friend class nsAutoPtr<NormalJSContext>; + + static const JSClass sGlobalClass; + static const uint32_t kContextHeapSize = 768 * 1024; + + JSContext* mContext; + JSObject* mGlobal; + +public: + static NormalJSContext* + Create(); + + JSContext* + Context() const + { + return mContext; + } + + JSObject* + Global() const + { + return mGlobal; + } + +protected: + NormalJSContext() + : mContext(nullptr) + , mGlobal(nullptr) + { + MOZ_COUNT_CTOR(NormalJSContext); + } + + ~NormalJSContext() + { + MOZ_COUNT_DTOR(NormalJSContext); + + if (mContext) { + JS_DestroyContext(mContext); + } + } + + bool + Init(); +}; + +class CreateIndexOp::ThreadLocalJSContext final + : public NormalJSContext +{ + friend class CreateIndexOp; + friend class nsAutoPtr<ThreadLocalJSContext>; + +public: + static ThreadLocalJSContext* + GetOrCreate(); + +private: + ThreadLocalJSContext() + { + MOZ_COUNT_CTOR(CreateIndexOp::ThreadLocalJSContext); + } + + ~ThreadLocalJSContext() + { + MOZ_COUNT_DTOR(CreateIndexOp::ThreadLocalJSContext); + } +}; + +class CreateIndexOp::UpdateIndexDataValuesFunction final + : public mozIStorageFunction +{ + RefPtr<CreateIndexOp> mOp; + RefPtr<DatabaseConnection> mConnection; + JSContext* mCx; + +public: + UpdateIndexDataValuesFunction(CreateIndexOp* aOp, + DatabaseConnection* aConnection, + JSContext* aCx) + : mOp(aOp) + , mConnection(aConnection) + , mCx(aCx) + { + MOZ_ASSERT(aOp); + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aCx); + } + + NS_DECL_ISUPPORTS + +private: + ~UpdateIndexDataValuesFunction() + { } + + NS_DECL_MOZISTORAGEFUNCTION +}; + +class DeleteIndexOp final + : public VersionChangeTransactionOp +{ + friend class VersionChangeTransaction; + + const int64_t mObjectStoreId; + const int64_t mIndexId; + const bool mUnique; + const bool mIsLastIndex; + +private: + // Only created by VersionChangeTransaction. + DeleteIndexOp(VersionChangeTransaction* aTransaction, + const int64_t aObjectStoreId, + const int64_t aIndexId, + const bool aUnique, + const bool aIsLastIndex); + + ~DeleteIndexOp() + { } + + nsresult + RemoveReferencesToIndex(DatabaseConnection* aConnection, + const Key& aObjectDataKey, + nsTArray<IndexDataValue>& aIndexValues); + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class RenameIndexOp final + : public VersionChangeTransactionOp +{ + friend class VersionChangeTransaction; + + const int64_t mObjectStoreId; + const int64_t mIndexId; + const nsString mNewName; + +private: + // Only created by VersionChangeTransaction. + RenameIndexOp(VersionChangeTransaction* aTransaction, + FullIndexMetadata* const aMetadata, + int64_t aObjectStoreId) + : VersionChangeTransactionOp(aTransaction) + , mObjectStoreId(aObjectStoreId) + , mIndexId(aMetadata->mCommonMetadata.id()) + , mNewName(aMetadata->mCommonMetadata.name()) + { + MOZ_ASSERT(mIndexId); + } + + ~RenameIndexOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class NormalTransactionOp + : public TransactionDatabaseOperationBase + , public PBackgroundIDBRequestParent +{ +#ifdef DEBUG + bool mResponseSent; +#endif + +public: + virtual void + Cleanup() override; + +protected: + explicit NormalTransactionOp(TransactionBase* aTransaction) + : TransactionDatabaseOperationBase(aTransaction) +#ifdef DEBUG + , mResponseSent(false) +#endif + { } + + virtual + ~NormalTransactionOp() + { } + + // An overload of DatabaseOperationBase's function that can avoid doing extra + // work on non-versionchange transactions. + static nsresult + ObjectStoreHasIndexes(NormalTransactionOp* aOp, + DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const bool aMayHaveIndexes, + bool* aHasIndexes); + + virtual nsresult + GetPreprocessParams(PreprocessParams& aParams); + + + // Subclasses use this override to set the IPDL response value. + virtual void + GetResponse(RequestResponse& aResponse) = 0; + +private: + virtual nsresult + SendPreprocessInfo() override; + + virtual nsresult + SendSuccessResult() override; + + virtual bool + SendFailureResult(nsresult aResultCode) override; + + // IPDL methods. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvContinue(const PreprocessResponse& aResponse) override; +}; + +class ObjectStoreAddOrPutRequestOp final + : public NormalTransactionOp +{ + friend class TransactionBase; + + typedef mozilla::dom::quota::PersistenceType PersistenceType; + + struct StoredFileInfo; + class SCInputStream; + + const ObjectStoreAddPutParams mParams; + Maybe<UniqueIndexTable> mUniqueIndexTable; + + // This must be non-const so that we can update the mNextAutoIncrementId field + // if we are modifying an autoIncrement objectStore. + RefPtr<FullObjectStoreMetadata> mMetadata; + + FallibleTArray<StoredFileInfo> mStoredFileInfos; + + Key mResponse; + const nsCString mGroup; + const nsCString mOrigin; + const PersistenceType mPersistenceType; + const bool mOverwrite; + bool mObjectStoreMayHaveIndexes; + bool mDataOverThreshold; + +private: + // Only created by TransactionBase. + ObjectStoreAddOrPutRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams); + + ~ObjectStoreAddOrPutRequestOp() + { } + + nsresult + RemoveOldIndexDataValues(DatabaseConnection* aConnection); + + virtual bool + Init(TransactionBase* aTransaction) override; + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override; + + virtual void + Cleanup() override; +}; + +struct ObjectStoreAddOrPutRequestOp::StoredFileInfo final +{ + RefPtr<DatabaseFile> mFileActor; + RefPtr<FileInfo> mFileInfo; + // A non-Blob-backed inputstream to write to disk. If null, mFileActor may + // still have a stream for us to write. + nsCOMPtr<nsIInputStream> mInputStream; + StructuredCloneFile::FileType mType; + + StoredFileInfo() + : mType(StructuredCloneFile::eBlob) + { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); + } + + ~StoredFileInfo() + { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); + } + + void + Serialize(nsString& aText) + { + MOZ_ASSERT(mFileInfo); + + const int64_t id = mFileInfo->Id(); + + switch (mType) { + case StructuredCloneFile::eBlob: + aText.AppendInt(id); + break; + + case StructuredCloneFile::eMutableFile: + aText.AppendInt(-id); + break; + + case StructuredCloneFile::eStructuredClone: + aText.Append('.'); + aText.AppendInt(id); + break; + + case StructuredCloneFile::eWasmBytecode: + aText.Append('/'); + aText.AppendInt(id); + break; + + case StructuredCloneFile::eWasmCompiled: + aText.Append('\\'); + aText.AppendInt(id); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + } +}; + +class ObjectStoreAddOrPutRequestOp::SCInputStream final + : public nsIInputStream +{ + const JSStructuredCloneData& mData; + JSStructuredCloneData::IterImpl mIter; + +public: + explicit SCInputStream(const JSStructuredCloneData& aData) + : mData(aData) + , mIter(aData.Iter()) + { } + +private: + virtual ~SCInputStream() + { } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM +}; + +class ObjectStoreGetRequestOp final + : public NormalTransactionOp +{ + friend class TransactionBase; + + const uint32_t mObjectStoreId; + RefPtr<Database> mDatabase; + const OptionalKeyRange mOptionalKeyRange; + AutoTArray<StructuredCloneReadInfo, 1> mResponse; + PBackgroundParent* mBackgroundParent; + uint32_t mPreprocessInfoCount; + const uint32_t mLimit; + const bool mGetAll; + +private: + // Only created by TransactionBase. + ObjectStoreGetRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll); + + ~ObjectStoreGetRequestOp() + { } + + template <bool aForPreprocess, typename T> + nsresult + ConvertResponse(StructuredCloneReadInfo& aInfo, T& aResult); + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual bool + HasPreprocessInfo() override; + + virtual nsresult + GetPreprocessParams(PreprocessParams& aParams) override; + + virtual void + GetResponse(RequestResponse& aResponse) override; +}; + +class ObjectStoreGetKeyRequestOp final + : public NormalTransactionOp +{ + friend class TransactionBase; + + const uint32_t mObjectStoreId; + const OptionalKeyRange mOptionalKeyRange; + const uint32_t mLimit; + const bool mGetAll; + FallibleTArray<Key> mResponse; + +private: + // Only created by TransactionBase. + ObjectStoreGetKeyRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll); + + ~ObjectStoreGetKeyRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override; +}; + +class ObjectStoreDeleteRequestOp final + : public NormalTransactionOp +{ + friend class TransactionBase; + + const ObjectStoreDeleteParams mParams; + ObjectStoreDeleteResponse mResponse; + bool mObjectStoreMayHaveIndexes; + +private: + ObjectStoreDeleteRequestOp(TransactionBase* aTransaction, + const ObjectStoreDeleteParams& aParams); + + ~ObjectStoreDeleteRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override + { + aResponse = Move(mResponse); + } +}; + +class ObjectStoreClearRequestOp final + : public NormalTransactionOp +{ + friend class TransactionBase; + + const ObjectStoreClearParams mParams; + ObjectStoreClearResponse mResponse; + bool mObjectStoreMayHaveIndexes; + +private: + ObjectStoreClearRequestOp(TransactionBase* aTransaction, + const ObjectStoreClearParams& aParams); + + ~ObjectStoreClearRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override + { + aResponse = Move(mResponse); + } +}; + +class ObjectStoreCountRequestOp final + : public NormalTransactionOp +{ + friend class TransactionBase; + + const ObjectStoreCountParams mParams; + ObjectStoreCountResponse mResponse; + +private: + ObjectStoreCountRequestOp(TransactionBase* aTransaction, + const ObjectStoreCountParams& aParams) + : NormalTransactionOp(aTransaction) + , mParams(aParams) + { } + + ~ObjectStoreCountRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override + { + aResponse = Move(mResponse); + } +}; + +class IndexRequestOpBase + : public NormalTransactionOp +{ +protected: + const RefPtr<FullIndexMetadata> mMetadata; + +protected: + IndexRequestOpBase(TransactionBase* aTransaction, + const RequestParams& aParams) + : NormalTransactionOp(aTransaction) + , mMetadata(IndexMetadataForParams(aTransaction, aParams)) + { } + + virtual + ~IndexRequestOpBase() + { } + +private: + static already_AddRefed<FullIndexMetadata> + IndexMetadataForParams(TransactionBase* aTransaction, + const RequestParams& aParams); +}; + +class IndexGetRequestOp final + : public IndexRequestOpBase +{ + friend class TransactionBase; + + RefPtr<Database> mDatabase; + const OptionalKeyRange mOptionalKeyRange; + AutoTArray<StructuredCloneReadInfo, 1> mResponse; + PBackgroundParent* mBackgroundParent; + const uint32_t mLimit; + const bool mGetAll; + +private: + // Only created by TransactionBase. + IndexGetRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll); + + ~IndexGetRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override; +}; + +class IndexGetKeyRequestOp final + : public IndexRequestOpBase +{ + friend class TransactionBase; + + const OptionalKeyRange mOptionalKeyRange; + AutoTArray<Key, 1> mResponse; + const uint32_t mLimit; + const bool mGetAll; + +private: + // Only created by TransactionBase. + IndexGetKeyRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll); + + ~IndexGetKeyRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override; +}; + +class IndexCountRequestOp final + : public IndexRequestOpBase +{ + friend class TransactionBase; + + const IndexCountParams mParams; + IndexCountResponse mResponse; + +private: + // Only created by TransactionBase. + IndexCountRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams) + : IndexRequestOpBase(aTransaction, aParams) + , mParams(aParams.get_IndexCountParams()) + { } + + ~IndexCountRequestOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual void + GetResponse(RequestResponse& aResponse) override + { + aResponse = Move(mResponse); + } +}; + +class Cursor final : + public PBackgroundIDBCursorParent +{ + friend class TransactionBase; + + class ContinueOp; + class CursorOpBase; + class OpenOp; + +public: + typedef OpenCursorParams::Type Type; + +private: + RefPtr<TransactionBase> mTransaction; + RefPtr<Database> mDatabase; + RefPtr<FileManager> mFileManager; + PBackgroundParent* mBackgroundParent; + + // These should only be touched on the PBackground thread to check whether the + // objectStore or index has been deleted. Holding these saves a hash lookup + // for every call to continue()/advance(). + RefPtr<FullObjectStoreMetadata> mObjectStoreMetadata; + RefPtr<FullIndexMetadata> mIndexMetadata; + + const int64_t mObjectStoreId; + const int64_t mIndexId; + + nsCString mContinueQuery; + nsCString mContinueToQuery; + nsCString mContinuePrimaryKeyQuery; + nsCString mLocale; + + Key mKey; + Key mObjectKey; + Key mRangeKey; + Key mSortKey; + + CursorOpBase* mCurrentlyRunningOp; + + const Type mType; + const Direction mDirection; + + const bool mUniqueIndex; + const bool mIsSameProcessActor; + bool mActorDestroyed; + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Cursor) + +private: + // Only created by TransactionBase. + Cursor(TransactionBase* aTransaction, + Type aType, + FullObjectStoreMetadata* aObjectStoreMetadata, + FullIndexMetadata* aIndexMetadata, + Direction aDirection); + + // Reference counted. + ~Cursor() + { + MOZ_ASSERT(mActorDestroyed); + } + + bool + VerifyRequestParams(const CursorRequestParams& aParams) const; + + // Only called by TransactionBase. + bool + Start(const OpenCursorParams& aParams); + + void + SendResponseInternal( + CursorResponse& aResponse, + const nsTArray<FallibleTArray<StructuredCloneFile>>& aFiles); + + // Must call SendResponseInternal! + bool + SendResponse(const CursorResponse& aResponse) = delete; + + // IPDL methods. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvDeleteMe() override; + + virtual bool + RecvContinue(const CursorRequestParams& aParams) override; + + bool + IsLocaleAware() const { + return !mLocale.IsEmpty(); + } +}; + +class Cursor::CursorOpBase + : public TransactionDatabaseOperationBase +{ +protected: + RefPtr<Cursor> mCursor; + nsTArray<FallibleTArray<StructuredCloneFile>> mFiles; + + CursorResponse mResponse; + +#ifdef DEBUG + bool mResponseSent; +#endif + +protected: + explicit CursorOpBase(Cursor* aCursor) + : TransactionDatabaseOperationBase(aCursor->mTransaction) + , mCursor(aCursor) +#ifdef DEBUG + , mResponseSent(false) +#endif + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aCursor); + } + + virtual + ~CursorOpBase() + { } + + virtual bool + SendFailureResult(nsresult aResultCode) override; + + virtual void + Cleanup() override; + + nsresult + PopulateResponseFromStatement(DatabaseConnection::CachedStatement& aStmt, + bool aInitializeResponse); +}; + +class Cursor::OpenOp final + : public Cursor::CursorOpBase +{ + friend class Cursor; + + const OptionalKeyRange mOptionalKeyRange; + +private: + // Only created by Cursor. + OpenOp(Cursor* aCursor, + const OptionalKeyRange& aOptionalKeyRange) + : CursorOpBase(aCursor) + , mOptionalKeyRange(aOptionalKeyRange) + { } + + // Reference counted. + ~OpenOp() + { } + + void + GetRangeKeyInfo(bool aLowerBound, Key* aKey, bool* aOpen); + + nsresult + DoObjectStoreDatabaseWork(DatabaseConnection* aConnection); + + nsresult + DoObjectStoreKeyDatabaseWork(DatabaseConnection* aConnection); + + nsresult + DoIndexDatabaseWork(DatabaseConnection* aConnection); + + nsresult + DoIndexKeyDatabaseWork(DatabaseConnection* aConnection); + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual nsresult + SendSuccessResult() override; +}; + +class Cursor::ContinueOp final + : public Cursor::CursorOpBase +{ + friend class Cursor; + + const CursorRequestParams mParams; + +private: + // Only created by Cursor. + ContinueOp(Cursor* aCursor, + const CursorRequestParams& aParams) + : CursorOpBase(aCursor) + , mParams(aParams) + { + MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); + } + + // Reference counted. + ~ContinueOp() + { } + + virtual nsresult + DoDatabaseWork(DatabaseConnection* aConnection) override; + + virtual nsresult + SendSuccessResult() override; +}; + +class Utils final + : public PBackgroundIndexedDBUtilsParent +{ +#ifdef DEBUG + bool mActorDestroyed; +#endif + +public: + Utils(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::Utils) + +private: + // Reference counted. + ~Utils(); + + // IPDL methods are only called by IPDL. + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool + RecvDeleteMe() override; + + virtual bool + RecvGetFileReferences(const PersistenceType& aPersistenceType, + const nsCString& aOrigin, + const nsString& aDatabaseName, + const int64_t& aFileId, + int32_t* aRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt, + bool* aResult) override; +}; + +class GetFileReferencesHelper final + : public Runnable +{ + PersistenceType mPersistenceType; + nsCString mOrigin; + nsString mDatabaseName; + int64_t mFileId; + + mozilla::Mutex mMutex; + mozilla::CondVar mCondVar; + int32_t mMemRefCnt; + int32_t mDBRefCnt; + int32_t mSliceRefCnt; + bool mResult; + bool mWaiting; + +public: + GetFileReferencesHelper(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName, + int64_t aFileId) + : mPersistenceType(aPersistenceType) + , mOrigin(aOrigin) + , mDatabaseName(aDatabaseName) + , mFileId(aFileId) + , mMutex("GetFileReferencesHelper::mMutex") + , mCondVar(mMutex, "GetFileReferencesHelper::mCondVar") + , mMemRefCnt(-1) + , mDBRefCnt(-1) + , mSliceRefCnt(-1) + , mResult(false) + , mWaiting(true) + { } + + nsresult + DispatchAndReturnFileReferences(int32_t* aMemRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt, + bool* aResult); + +private: + ~GetFileReferencesHelper() {} + + NS_DECL_NSIRUNNABLE +}; + +class FlushPendingFileDeletionsRunnable final + : public Runnable +{ +private: + ~FlushPendingFileDeletionsRunnable() + { } + + NS_DECL_NSIRUNNABLE +}; + +class PermissionRequestHelper final + : public PermissionRequestBase + , public PIndexedDBPermissionRequestParent +{ + bool mActorDestroyed; + +public: + PermissionRequestHelper(Element* aOwnerElement, + nsIPrincipal* aPrincipal) + : PermissionRequestBase(aOwnerElement, aPrincipal) + , mActorDestroyed(false) + { } + +protected: + ~PermissionRequestHelper() + { } + +private: + virtual void + OnPromptComplete(PermissionValue aPermissionValue) override; + + virtual void + ActorDestroy(ActorDestroyReason aWhy) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +struct DatabaseActorInfo final +{ + friend class nsAutoPtr<DatabaseActorInfo>; + + RefPtr<FullDatabaseMetadata> mMetadata; + nsTArray<Database*> mLiveDatabases; + RefPtr<FactoryOp> mWaitingFactoryOp; + + DatabaseActorInfo(FullDatabaseMetadata* aMetadata, + Database* aDatabase) + : mMetadata(aMetadata) + { + MOZ_ASSERT(aDatabase); + + MOZ_COUNT_CTOR(DatabaseActorInfo); + + mLiveDatabases.AppendElement(aDatabase); + } + +private: + ~DatabaseActorInfo() + { + MOZ_ASSERT(mLiveDatabases.IsEmpty()); + MOZ_ASSERT(!mWaitingFactoryOp || + !mWaitingFactoryOp->HasBlockedDatabases()); + + MOZ_COUNT_DTOR(DatabaseActorInfo); + } +}; + +class DatabaseLoggingInfo final +{ +#ifdef DEBUG + // Just for potential warnings. + friend class Factory; +#endif + + LoggingInfo mLoggingInfo; + +public: + explicit + DatabaseLoggingInfo(const LoggingInfo& aLoggingInfo) + : mLoggingInfo(aLoggingInfo) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aLoggingInfo.nextTransactionSerialNumber()); + MOZ_ASSERT(aLoggingInfo.nextVersionChangeTransactionSerialNumber()); + MOZ_ASSERT(aLoggingInfo.nextRequestSerialNumber()); + } + + const nsID& + Id() const + { + AssertIsOnBackgroundThread(); + + return mLoggingInfo.backgroundChildLoggingId(); + } + + int64_t + NextTransactionSN(IDBTransaction::Mode aMode) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mLoggingInfo.nextTransactionSerialNumber() < INT64_MAX); + MOZ_ASSERT(mLoggingInfo.nextVersionChangeTransactionSerialNumber() > + INT64_MIN); + + if (aMode == IDBTransaction::VERSION_CHANGE) { + return mLoggingInfo.nextVersionChangeTransactionSerialNumber()--; + } + + return mLoggingInfo.nextTransactionSerialNumber()++; + } + + uint64_t + NextRequestSN() + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mLoggingInfo.nextRequestSerialNumber() < UINT64_MAX); + + return mLoggingInfo.nextRequestSerialNumber()++; + } + + NS_INLINE_DECL_REFCOUNTING(DatabaseLoggingInfo) + +private: + ~DatabaseLoggingInfo(); +}; + +class BlobImplStoredFile final + : public BlobImplFile +{ + RefPtr<FileInfo> mFileInfo; + const bool mSnapshot; + +public: + BlobImplStoredFile(nsIFile* aFile, FileInfo* aFileInfo, bool aSnapshot) + : BlobImplFile(aFile) + , mFileInfo(aFileInfo) + , mSnapshot(aSnapshot) + { + AssertIsOnBackgroundThread(); + + // Getting the content type is not currently supported off the main thread. + // This isn't a problem here because: + // + // 1. The real content type is stored in the structured clone data and + // that's all that the DOM will see. This blob's data will be updated + // during RecvSetMysteryBlobInfo(). + // 2. The nsExternalHelperAppService guesses the content type based only + // on the file extension. Our stored files have no extension so the + // current code path fails and sets the content type to the empty + // string. + // + // So, this is a hack to keep the nsExternalHelperAppService out of the + // picture entirely. Eventually we should probably fix this some other way. + mContentType.Truncate(); + mIsFile = false; + } + + bool + IsShareable(FileManager* aFileManager) const + { + AssertIsOnBackgroundThread(); + + return mFileInfo->Manager() == aFileManager && !mSnapshot; + } + + FileInfo* + GetFileInfo() const + { + AssertIsOnBackgroundThread(); + + return mFileInfo; + } + +private: + ~BlobImplStoredFile() + { } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECLARE_STATIC_IID_ACCESSOR(BLOB_IMPL_STORED_FILE_IID) + + virtual int64_t + GetFileId() override + { + MOZ_ASSERT(mFileInfo); + + return mFileInfo->Id(); + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(BlobImplStoredFile, BLOB_IMPL_STORED_FILE_IID) + +class QuotaClient final + : public mozilla::dom::quota::Client +{ + static QuotaClient* sInstance; + + nsCOMPtr<nsIEventTarget> mBackgroundThread; + nsTArray<RefPtr<Maintenance>> mMaintenanceQueue; + RefPtr<Maintenance> mCurrentMaintenance; + RefPtr<nsThreadPool> mMaintenanceThreadPool; + bool mShutdownRequested; + +public: + QuotaClient(); + + static QuotaClient* + GetInstance() + { + AssertIsOnBackgroundThread(); + + return sInstance; + } + + static bool + IsShuttingDownOnBackgroundThread() + { + AssertIsOnBackgroundThread(); + + if (sInstance) { + return sInstance->IsShuttingDown(); + } + + return QuotaManager::IsShuttingDown(); + } + + static bool + IsShuttingDownOnNonBackgroundThread() + { + MOZ_ASSERT(!IsOnBackgroundThread()); + + return QuotaManager::IsShuttingDown(); + } + + nsIEventTarget* + BackgroundThread() const + { + MOZ_ASSERT(mBackgroundThread); + return mBackgroundThread; + } + + bool + IsShuttingDown() const + { + AssertIsOnBackgroundThread(); + + return mShutdownRequested; + } + + already_AddRefed<Maintenance> + GetCurrentMaintenance() const + { + RefPtr<Maintenance> result = mCurrentMaintenance; + return result.forget(); + } + + void + NoteFinishedMaintenance(Maintenance* aMaintenance) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aMaintenance); + MOZ_ASSERT(mCurrentMaintenance == aMaintenance); + + mCurrentMaintenance = nullptr; + ProcessMaintenanceQueue(); + } + + nsThreadPool* + GetOrCreateThreadPool(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QuotaClient, override) + + virtual mozilla::dom::quota::Client::Type + GetType() override; + + virtual nsresult + InitOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) override; + + virtual nsresult + GetUsageForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) override; + + virtual void + OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) + override; + + virtual void + ReleaseIOThreadObjects() override; + + virtual void + AbortOperations(const nsACString& aOrigin) override; + + virtual void + AbortOperationsForProcess(ContentParentId aContentParentId) override; + + virtual void + StartIdleMaintenance() override; + + virtual void + StopIdleMaintenance() override; + + virtual void + ShutdownWorkThreads() override; + + virtual void + DidInitialize(QuotaManager* aQuotaManager) override; + + virtual void + WillShutdown() override; + +private: + ~QuotaClient(); + + nsresult + GetDirectory(PersistenceType aPersistenceType, + const nsACString& aOrigin, + nsIFile** aDirectory); + + nsresult + GetUsageForDirectoryInternal(nsIFile* aDirectory, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo, + bool aDatabaseFiles); + + // Runs on the PBackground thread. Checks to see if there's a queued + // Maintenance to run. + void + ProcessMaintenanceQueue(); +}; + +class Maintenance final + : public Runnable + , public OpenDirectoryListener +{ + struct DirectoryInfo; + + enum class State + { + // Newly created on the PBackground thread. Will proceed immediately or be + // added to the maintenance queue. The next step is either + // DirectoryOpenPending if IndexedDatabaseManager is running, or + // CreateIndexedDatabaseManager if not. + Initial = 0, + + // Create IndexedDatabaseManager on the main thread. The next step is either + // Finishing if IndexedDatabaseManager initialization fails, or + // IndexedDatabaseManagerOpen if initialization succeeds. + CreateIndexedDatabaseManager, + + // Call OpenDirectory() on the PBackground thread. The next step is + // DirectoryOpenPending. + IndexedDatabaseManagerOpen, + + // Waiting for directory open allowed on the PBackground thread. The next + // step is either Finishing if directory lock failed to acquire, or + // DirectoryWorkOpen if directory lock is acquired. + DirectoryOpenPending, + + // Waiting to do/doing work on the QuotaManager IO thread. The next step is + // BeginDatabaseMaintenance. + DirectoryWorkOpen, + + // Dispatching a runnable for each database on the PBackground thread. The + // next state is either WaitingForDatabaseMaintenancesToComplete if at least + // one runnable has been dispatched, or Finishing otherwise. + BeginDatabaseMaintenance, + + // Waiting for DatabaseMaintenance to finish on maintenance thread pool. + // The next state is Finishing if the last runnable has finished. + WaitingForDatabaseMaintenancesToComplete, + + // Waiting to finish/finishing on the PBackground thread. The next step is + // Completed. + Finishing, + + // All done. + Complete + }; + + RefPtr<QuotaClient> mQuotaClient; + PRTime mStartTime; + RefPtr<DirectoryLock> mDirectoryLock; + nsTArray<DirectoryInfo> mDirectoryInfos; + nsDataHashtable<nsStringHashKey, DatabaseMaintenance*> mDatabaseMaintenances; + nsresult mResultCode; + Atomic<bool> mAborted; + State mState; + +public: + explicit Maintenance(QuotaClient* aQuotaClient) + : mQuotaClient(aQuotaClient) + , mStartTime(PR_Now()) + , mResultCode(NS_OK) + , mAborted(false) + , mState(State::Initial) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aQuotaClient); + MOZ_ASSERT(QuotaClient::GetInstance() == aQuotaClient); + MOZ_ASSERT(mStartTime); + } + + nsIEventTarget* + BackgroundThread() const + { + MOZ_ASSERT(mQuotaClient); + return mQuotaClient->BackgroundThread(); + } + + PRTime + StartTime() const + { + return mStartTime; + } + + bool + IsAborted() const + { + return mAborted; + } + + void + RunImmediately() + { + MOZ_ASSERT(mState == State::Initial); + + Unused << this->Run(); + } + + void + Abort() + { + AssertIsOnBackgroundThread(); + + mAborted = true; + } + + void + RegisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); + + void + UnregisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); + + already_AddRefed<DatabaseMaintenance> + GetDatabaseMaintenance(const nsAString& aDatabasePath) const + { + AssertIsOnBackgroundThread(); + + RefPtr<DatabaseMaintenance> result = + mDatabaseMaintenances.Get(aDatabasePath); + return result.forget(); + } + +private: + ~Maintenance() + { + MOZ_ASSERT(mState == State::Complete); + MOZ_ASSERT(!mDatabaseMaintenances.Count()); + } + + // Runs on the PBackground thread. Checks if IndexedDatabaseManager is + // running. Calls OpenDirectory() or dispatches to the main thread on which + // CreateIndexedDatabaseManager() is called. + nsresult + Start(); + + // Runs on the main thread. Once IndexedDatabaseManager is created it will + // dispatch to the PBackground thread on which OpenDirectory() is called. + nsresult + CreateIndexedDatabaseManager(); + + // Runs on the PBackground thread. Once QuotaManager has given a lock it will + // call DirectoryOpen(). + nsresult + OpenDirectory(); + + // Runs on the PBackground thread. Dispatches to the QuotaManager I/O thread. + nsresult + DirectoryOpen(); + + // Runs on the QuotaManager I/O thread. Once it finds databases it will + // dispatch to the PBackground thread on which BeginDatabaseMaintenance() + // is called. + nsresult + DirectoryWork(); + + // Runs on the PBackground thread. It dispatches a runnable for each database. + nsresult + BeginDatabaseMaintenance(); + + // Runs on the PBackground thread. Called when the maintenance is finished or + // if any of above methods fails. + void + Finish(); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIRUNNABLE + + // OpenDirectoryListener overrides. + virtual void + DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void + DirectoryLockFailed() override; +}; + +struct Maintenance::DirectoryInfo final +{ + const nsCString mGroup; + const nsCString mOrigin; + nsTArray<nsString> mDatabasePaths; + const PersistenceType mPersistenceType; + + DirectoryInfo(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + nsTArray<nsString>&& aDatabasePaths) + : mGroup(aGroup) + , mOrigin(aOrigin) + , mDatabasePaths(Move(aDatabasePaths)) + , mPersistenceType(aPersistenceType) + { + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT(!aGroup.IsEmpty()); + MOZ_ASSERT(!aOrigin.IsEmpty()); +#ifdef DEBUG + MOZ_ASSERT(!mDatabasePaths.IsEmpty()); + for (const nsString& databasePath : mDatabasePaths) { + MOZ_ASSERT(!databasePath.IsEmpty()); + } +#endif + + MOZ_COUNT_CTOR(Maintenance::DirectoryInfo); + } + + DirectoryInfo(DirectoryInfo&& aOther) + : mGroup(Move(aOther.mGroup)) + , mOrigin(Move(aOther.mOrigin)) + , mDatabasePaths(Move(aOther.mDatabasePaths)) + , mPersistenceType(Move(aOther.mPersistenceType)) + { +#ifdef DEBUG + MOZ_ASSERT(!mDatabasePaths.IsEmpty()); + for (const nsString& databasePath : mDatabasePaths) { + MOZ_ASSERT(!databasePath.IsEmpty()); + } +#endif + + MOZ_COUNT_CTOR(Maintenance::DirectoryInfo); + } + + ~DirectoryInfo() + { + MOZ_COUNT_DTOR(Maintenance::DirectoryInfo); + } + + DirectoryInfo(const DirectoryInfo& aOther) = delete; +}; + +class DatabaseMaintenance final + : public Runnable +{ + // The minimum amount of time that has passed since the last vacuum before we + // will attempt to analyze the database for fragmentation. + static const PRTime kMinVacuumAge = + PRTime(PR_USEC_PER_SEC) * 60 * 60 * 24 * 7; + + // If the percent of database pages that are not in contiguous order is higher + // than this percentage we will attempt a vacuum. + static const int32_t kPercentUnorderedThreshold = 30; + + // If the percent of file size growth since the last vacuum is higher than + // this percentage we will attempt a vacuum. + static const int32_t kPercentFileSizeGrowthThreshold = 10; + + // The number of freelist pages beyond which we will favor an incremental + // vacuum over a full vacuum. + static const int32_t kMaxFreelistThreshold = 5; + + // If the percent of unused file bytes in the database exceeds this percentage + // then we will attempt a full vacuum. + static const int32_t kPercentUnusedThreshold = 20; + + class AutoProgressHandler; + + enum class MaintenanceAction + { + Nothing = 0, + IncrementalVacuum, + FullVacuum + }; + + RefPtr<Maintenance> mMaintenance; + const nsCString mGroup; + const nsCString mOrigin; + const nsString mDatabasePath; + nsCOMPtr<nsIRunnable> mCompleteCallback; + const PersistenceType mPersistenceType; + +public: + DatabaseMaintenance(Maintenance* aMaintenance, + PersistenceType aPersistenceType, + const nsCString& aGroup, + const nsCString& aOrigin, + const nsString& aDatabasePath) + : mMaintenance(aMaintenance) + , mGroup(aGroup) + , mOrigin(aOrigin) + , mDatabasePath(aDatabasePath) + , mPersistenceType(aPersistenceType) + { } + + const nsString& + DatabasePath() const + { + return mDatabasePath; + } + + void + WaitForCompletion(nsIRunnable* aCallback) + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCompleteCallback); + + mCompleteCallback = aCallback; + } + +private: + ~DatabaseMaintenance() + { } + + // Runs on maintenance thread pool. Does maintenance on the database. + void + PerformMaintenanceOnDatabase(); + + // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. + nsresult + CheckIntegrity(mozIStorageConnection* aConnection, bool* aOk); + + // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. + nsresult + DetermineMaintenanceAction(mozIStorageConnection* aConnection, + nsIFile* aDatabaseFile, + MaintenanceAction* aMaintenanceAction); + + // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. + void + IncrementalVacuum(mozIStorageConnection* aConnection); + + // Runs on maintenance thread pool as part of PerformMaintenanceOnDatabase. + void + FullVacuum(mozIStorageConnection* aConnection, + nsIFile* aDatabaseFile); + + // Runs on the PBackground thread. It dispatches a complete callback and + // unregisters from Maintenance. + void + RunOnOwningThread(); + + // Runs on maintenance thread pool. Once it performs database maintenance + // it will dispatch to the PBackground thread on which RunOnOwningThread() + // is called. + void + RunOnConnectionThread(); + + NS_DECL_NSIRUNNABLE +}; + +class MOZ_STACK_CLASS DatabaseMaintenance::AutoProgressHandler final + : public mozIStorageProgressHandler +{ + Maintenance* mMaintenance; + mozIStorageConnection* mConnection; + + NS_DECL_OWNINGTHREAD + +#ifdef DEBUG + // This class is stack-based so we never actually allow AddRef/Release to do + // anything. But we need to know if any consumer *thinks* that they have a + // reference to this object so we track the reference countin DEBUG builds. + nsrefcnt mDEBUGRefCnt; +#endif + +public: + explicit AutoProgressHandler(Maintenance* aMaintenance) + : mMaintenance(aMaintenance) + , mConnection(nullptr) +#ifdef DEBUG + , mDEBUGRefCnt(0) +#endif + { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); + MOZ_ASSERT(aMaintenance); + } + + ~AutoProgressHandler() + { + NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); + + if (mConnection) { + Unregister(); + } + + MOZ_ASSERT(!mDEBUGRefCnt); + } + + nsresult + Register(mozIStorageConnection* aConnection); + + // We don't want the mRefCnt member but this class does not "inherit" + // nsISupports. + NS_DECL_ISUPPORTS_INHERITED + +private: + void + Unregister(); + + NS_DECL_MOZISTORAGEPROGRESSHANDLER + + // Not available for the heap! + void* + operator new(size_t) = delete; + void* + operator new[](size_t) = delete; + void + operator delete(void*) = delete; + void + operator delete[](void*) = delete; +}; + +class IntString : public nsAutoString +{ +public: + explicit + IntString(int64_t aInteger) + { + AppendInt(aInteger); + } +}; + +#ifdef DEBUG + +class DEBUGThreadSlower final + : public nsIThreadObserver +{ +public: + DEBUGThreadSlower() + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(kDEBUGThreadSleepMS); + } + + NS_DECL_ISUPPORTS + +private: + ~DEBUGThreadSlower() + { + AssertIsOnBackgroundThread(); + } + + NS_DECL_NSITHREADOBSERVER +}; + +#endif // DEBUG + +/******************************************************************************* + * Helper classes + ******************************************************************************/ + +class MOZ_STACK_CLASS FileHelper final +{ + RefPtr<FileManager> mFileManager; + + nsCOMPtr<nsIFile> mFileDirectory; + nsCOMPtr<nsIFile> mJournalDirectory; + +public: + explicit FileHelper(FileManager* aFileManager) + : mFileManager(aFileManager) + { } + + nsresult + Init(); + + already_AddRefed<nsIFile> + GetFile(FileInfo* aFileInfo); + + already_AddRefed<nsIFile> + GetCheckedFile(FileInfo* aFileInfo); + + already_AddRefed<nsIFile> + GetJournalFile(FileInfo* aFileInfo); + + nsresult + CreateFileFromStream(nsIFile* aFile, + nsIFile* aJournalFile, + nsIInputStream* aInputStream, + bool aCompress); + + nsresult + ReplaceFile(nsIFile* aFile, + nsIFile* aNewFile, + nsIFile* aNewJournalFile); + + nsresult + RemoveFile(nsIFile* aFile, + nsIFile* aJournalFile); + + already_AddRefed<FileInfo> + GetNewFileInfo(); + +private: + nsresult + SyncCopy(nsIInputStream* aInputStream, + nsIOutputStream* aOutputStream, + char* aBuffer, + uint32_t aBufferSize); +}; + +/******************************************************************************* + * Helper Functions + ******************************************************************************/ + +bool +TokenizerIgnoreNothing(char16_t /* aChar */) +{ + return false; +} + +nsresult +DeserializeStructuredCloneFile(FileManager* aFileManager, + const nsString& aText, + StructuredCloneFile* aFile) +{ + MOZ_ASSERT(!aText.IsEmpty()); + MOZ_ASSERT(aFile); + + StructuredCloneFile::FileType type; + + switch (aText.First()) { + case char16_t('-'): + type = StructuredCloneFile::eMutableFile; + break; + + case char16_t('.'): + type = StructuredCloneFile::eStructuredClone; + break; + + case char16_t('/'): + type = StructuredCloneFile::eWasmBytecode; + break; + + case char16_t('\\'): + type = StructuredCloneFile::eWasmCompiled; + break; + + default: + type = StructuredCloneFile::eBlob; + } + + nsresult rv; + int32_t id; + + if (type == StructuredCloneFile::eBlob) { + id = aText.ToInteger(&rv); + } else { + nsString text(Substring(aText, 1)); + + id = text.ToInteger(&rv); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<FileInfo> fileInfo = aFileManager->GetFileInfo(id); + MOZ_ASSERT(fileInfo); + + aFile->mFileInfo.swap(fileInfo); + aFile->mType = type; + + return NS_OK; +} + +nsresult +CheckWasmModule(FileHelper* aFileHelper, + StructuredCloneFile* aBytecodeFile, + StructuredCloneFile* aCompiledFile) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFileHelper); + MOZ_ASSERT(aBytecodeFile); + MOZ_ASSERT(aCompiledFile); + MOZ_ASSERT(aBytecodeFile->mType == StructuredCloneFile::eWasmBytecode); + MOZ_ASSERT(aCompiledFile->mType == StructuredCloneFile::eWasmCompiled); + + nsCOMPtr<nsIFile> compiledFile = + aFileHelper->GetCheckedFile(aCompiledFile->mFileInfo); + if (NS_WARN_IF(!compiledFile)) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + + bool match; + { + ScopedPRFileDesc compiledFileDesc; + rv = compiledFile->OpenNSPRFileDesc(PR_RDONLY, + 0644, + &compiledFileDesc.rwget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JS::BuildIdCharVector buildId; + bool ok = GetBuildId(&buildId); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_FAILURE; + } + + match = JS::CompiledWasmModuleAssumptionsMatch(compiledFileDesc, + Move(buildId)); + } + if (match) { + return NS_OK; + } + + // Re-compile the module. It would be preferable to do this in the child + // (content process) instead of here in the parent, but that would be way more + // complex and without significant memory allocation or security benefits. + // See the discussion starting from + // https://bugzilla.mozilla.org/show_bug.cgi?id=1312808#c9 for more details. + nsCOMPtr<nsIFile> bytecodeFile = + aFileHelper->GetCheckedFile(aBytecodeFile->mFileInfo); + if (NS_WARN_IF(!bytecodeFile)) { + return NS_ERROR_FAILURE; + } + + ScopedPRFileDesc bytecodeFileDesc; + rv = bytecodeFile->OpenNSPRFileDesc(PR_RDONLY, + 0644, + &bytecodeFileDesc.rwget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JS::BuildIdCharVector buildId; + bool ok = GetBuildId(&buildId); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_FAILURE; + } + + RefPtr<JS::WasmModule> module = JS::DeserializeWasmModule(bytecodeFileDesc, + nullptr, + Move(buildId), + nullptr, + 0, + 0); + if (NS_WARN_IF(!module)) { + return NS_ERROR_FAILURE; + } + + size_t compiledSize; + module->serializedSize(nullptr, &compiledSize); + + UniquePtr<uint8_t[]> compiled(new (fallible) uint8_t[compiledSize]); + if (NS_WARN_IF(!compiled)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + module->serialize(nullptr, 0, compiled.get(), compiledSize); + + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewByteInputStream(getter_AddRefs(inputStream), + reinterpret_cast<const char*>(compiled.get()), + compiledSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<FileInfo> newFileInfo = aFileHelper->GetNewFileInfo(); + + nsCOMPtr<nsIFile> newFile = aFileHelper->GetFile(newFileInfo); + if (NS_WARN_IF(!newFile)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> newJournalFile = + aFileHelper->GetJournalFile(newFileInfo); + if (NS_WARN_IF(!newJournalFile)) { + return NS_ERROR_FAILURE; + } + + rv = aFileHelper->CreateFileFromStream(newFile, + newJournalFile, + inputStream, + /* aCompress */ false); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsresult rv2 = aFileHelper->RemoveFile(newFile, newJournalFile); + if (NS_WARN_IF(NS_FAILED(rv2))) { + return rv; + } + return rv; + } + + rv = aFileHelper->ReplaceFile(compiledFile, newFile, newJournalFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsresult rv2 = aFileHelper->RemoveFile(newFile, newJournalFile); + if (NS_WARN_IF(NS_FAILED(rv2))) { + return rv; + } + return rv; + } + + return NS_OK; +} + +nsresult +DeserializeStructuredCloneFiles(FileManager* aFileManager, + const nsAString& aText, + nsTArray<StructuredCloneFile>& aResult, + bool* aHasPreprocessInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + + nsCharSeparatedTokenizerTemplate<TokenizerIgnoreNothing> + tokenizer(aText, ' '); + + nsAutoString token; + nsresult rv; + Maybe<FileHelper> fileHelper; + + while (tokenizer.hasMoreTokens()) { + token = tokenizer.nextToken(); + MOZ_ASSERT(!token.IsEmpty()); + + StructuredCloneFile* file = aResult.AppendElement(); + rv = DeserializeStructuredCloneFile(aFileManager, token, file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aHasPreprocessInfo) { + continue; + } + + if (file->mType == StructuredCloneFile::eWasmBytecode) { + *aHasPreprocessInfo = true; + } + else if (file->mType == StructuredCloneFile::eWasmCompiled) { + if (fileHelper.isNothing()) { + fileHelper.emplace(aFileManager); + + rv = fileHelper->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + MOZ_ASSERT(aResult.Length() > 1); + MOZ_ASSERT(aResult[aResult.Length() - 2].mType == + StructuredCloneFile::eWasmBytecode); + + StructuredCloneFile* previousFile = &aResult[aResult.Length() - 2]; + + rv = CheckWasmModule(fileHelper.ptr(), previousFile, file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aHasPreprocessInfo = true; + } + } + + return NS_OK; +} + +bool +GetDatabaseBaseFilename(const nsAString& aFilename, + nsDependentSubstring& aDatabaseBaseFilename) +{ + MOZ_ASSERT(!aFilename.IsEmpty()); + MOZ_ASSERT(aDatabaseBaseFilename.IsEmpty()); + + NS_NAMED_LITERAL_STRING(sqlite, ".sqlite"); + + if (!StringEndsWith(aFilename, sqlite) || + aFilename.Length() == sqlite.Length()) { + return false; + } + + MOZ_ASSERT(aFilename.Length() > sqlite.Length()); + + aDatabaseBaseFilename.Rebind(aFilename, + 0, + aFilename.Length() - sqlite.Length()); + return true; +} + +nsresult +SerializeStructuredCloneFiles( + PBackgroundParent* aBackgroundActor, + Database* aDatabase, + const nsTArray<StructuredCloneFile>& aFiles, + bool aForPreprocess, + FallibleTArray<SerializedStructuredCloneFile>& aResult) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aBackgroundActor); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(aResult.IsEmpty()); + + if (aFiles.IsEmpty()) { + return NS_OK; + } + + FileManager* fileManager = aDatabase->GetFileManager(); + + nsCOMPtr<nsIFile> directory = fileManager->GetCheckedDirectory(); + if (NS_WARN_IF(!directory)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const uint32_t count = aFiles.Length(); + + if (NS_WARN_IF(!aResult.SetCapacity(count, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t index = 0; index < count; index++) { + const StructuredCloneFile& file = aFiles[index]; + + if (aForPreprocess && + file.mType != StructuredCloneFile::eWasmBytecode && + file.mType != StructuredCloneFile::eWasmCompiled) { + continue; + } + + const int64_t fileId = file.mFileInfo->Id(); + MOZ_ASSERT(fileId > 0); + + nsCOMPtr<nsIFile> nativeFile = + fileManager->GetCheckedFileForId(directory, fileId); + if (NS_WARN_IF(!nativeFile)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + switch (file.mType) { + case StructuredCloneFile::eBlob: { + RefPtr<BlobImpl> impl = new BlobImplStoredFile(nativeFile, + file.mFileInfo, + /* aSnapshot */ false); + + PBlobParent* actor = + BackgroundParent::GetOrCreateActorForBlobImpl(aBackgroundActor, impl); + if (!actor) { + // This can only fail if the child has crashed. + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); + MOZ_ASSERT(file); + + file->file() = actor; + file->type() = StructuredCloneFile::eBlob; + + break; + } + + case StructuredCloneFile::eMutableFile: { + if (aDatabase->IsFileHandleDisabled()) { + SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); + MOZ_ASSERT(file); + + file->file() = null_t(); + file->type() = StructuredCloneFile::eMutableFile; + } else { + RefPtr<MutableFile> actor = + MutableFile::Create(nativeFile, aDatabase, file.mFileInfo); + if (!actor) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // Transfer ownership to IPDL. + actor->SetActorAlive(); + + if (!aDatabase->SendPBackgroundMutableFileConstructor(actor, + EmptyString(), + EmptyString())) { + // This can only fail if the child has crashed. + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); + MOZ_ASSERT(file); + + file->file() = actor; + file->type() = StructuredCloneFile::eMutableFile; + } + + break; + } + + case StructuredCloneFile::eStructuredClone: { + SerializedStructuredCloneFile* file = aResult.AppendElement(fallible); + MOZ_ASSERT(file); + + file->file() = null_t(); + file->type() = StructuredCloneFile::eStructuredClone; + + break; + } + + case StructuredCloneFile::eWasmBytecode: + case StructuredCloneFile::eWasmCompiled: { + if (!aForPreprocess) { + SerializedStructuredCloneFile* serializedFile = + aResult.AppendElement(fallible); + MOZ_ASSERT(serializedFile); + + serializedFile->file() = null_t(); + serializedFile->type() = file.mType; + } else { + RefPtr<BlobImpl> impl = new BlobImplStoredFile(nativeFile, + file.mFileInfo, + /* aSnapshot */ false); + + PBlobParent* actor = + BackgroundParent::GetOrCreateActorForBlobImpl(aBackgroundActor, + impl); + if (!actor) { + // This can only fail if the child has crashed. + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + SerializedStructuredCloneFile* serializedFile = + aResult.AppendElement(fallible); + MOZ_ASSERT(serializedFile); + + serializedFile->file() = actor; + serializedFile->type() = file.mType; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + + return NS_OK; +} + +already_AddRefed<nsIFile> +GetFileForFileInfo(FileInfo* aFileInfo) +{ + FileManager* fileManager = aFileInfo->Manager(); + nsCOMPtr<nsIFile> directory = fileManager->GetDirectory(); + if (NS_WARN_IF(!directory)) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = fileManager->GetFileForId(directory, + aFileInfo->Id()); + if (NS_WARN_IF(!file)) { + return nullptr; + } + + return file.forget(); +} + +/******************************************************************************* + * Globals + ******************************************************************************/ + +// Counts the number of "live" Factory, FactoryOp and Database instances. +uint64_t gBusyCount = 0; + +typedef nsTArray<RefPtr<FactoryOp>> FactoryOpArray; + +StaticAutoPtr<FactoryOpArray> gFactoryOps; + +// Maps a database id to information about live database actors. +typedef nsClassHashtable<nsCStringHashKey, DatabaseActorInfo> + DatabaseActorHashtable; + +StaticAutoPtr<DatabaseActorHashtable> gLiveDatabaseHashtable; + +StaticRefPtr<ConnectionPool> gConnectionPool; + +StaticRefPtr<FileHandleThreadPool> gFileHandleThreadPool; + +typedef nsDataHashtable<nsIDHashKey, DatabaseLoggingInfo*> + DatabaseLoggingInfoHashtable; + +StaticAutoPtr<DatabaseLoggingInfoHashtable> gLoggingInfoHashtable; + +typedef nsDataHashtable<nsUint32HashKey, uint32_t> TelemetryIdHashtable; + +StaticAutoPtr<TelemetryIdHashtable> gTelemetryIdHashtable; + +// Protects all reads and writes to gTelemetryIdHashtable. +StaticAutoPtr<Mutex> gTelemetryIdMutex; + +#ifdef DEBUG + +StaticRefPtr<DEBUGThreadSlower> gDEBUGThreadSlower; + +#endif // DEBUG + + +void +IncreaseBusyCount() +{ + AssertIsOnBackgroundThread(); + + // If this is the first instance then we need to do some initialization. + if (!gBusyCount) { + MOZ_ASSERT(!gFactoryOps); + gFactoryOps = new FactoryOpArray(); + + MOZ_ASSERT(!gLiveDatabaseHashtable); + gLiveDatabaseHashtable = new DatabaseActorHashtable(); + + MOZ_ASSERT(!gLoggingInfoHashtable); + gLoggingInfoHashtable = new DatabaseLoggingInfoHashtable(); + +#ifdef DEBUG + if (kDEBUGThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) { + NS_WARNING("PBackground thread debugging enabled, priority has been " + "modified!"); + nsCOMPtr<nsISupportsPriority> thread = + do_QueryInterface(NS_GetCurrentThread()); + MOZ_ASSERT(thread); + + MOZ_ALWAYS_SUCCEEDS(thread->SetPriority(kDEBUGThreadPriority)); + } + + if (kDEBUGThreadSleepMS) { + NS_WARNING("PBackground thread debugging enabled, sleeping after every " + "event!"); + nsCOMPtr<nsIThreadInternal> thread = + do_QueryInterface(NS_GetCurrentThread()); + MOZ_ASSERT(thread); + + gDEBUGThreadSlower = new DEBUGThreadSlower(); + + MOZ_ALWAYS_SUCCEEDS(thread->AddObserver(gDEBUGThreadSlower)); + } +#endif // DEBUG + } + + gBusyCount++; +} + +void +DecreaseBusyCount() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gBusyCount); + + // Clean up if there are no more instances. + if (--gBusyCount == 0) { + MOZ_ASSERT(gLoggingInfoHashtable); + gLoggingInfoHashtable = nullptr; + + MOZ_ASSERT(gLiveDatabaseHashtable); + MOZ_ASSERT(!gLiveDatabaseHashtable->Count()); + gLiveDatabaseHashtable = nullptr; + + MOZ_ASSERT(gFactoryOps); + MOZ_ASSERT(gFactoryOps->IsEmpty()); + gFactoryOps = nullptr; + +#ifdef DEBUG + if (kDEBUGThreadPriority != nsISupportsPriority::PRIORITY_NORMAL) { + nsCOMPtr<nsISupportsPriority> thread = + do_QueryInterface(NS_GetCurrentThread()); + MOZ_ASSERT(thread); + + MOZ_ALWAYS_SUCCEEDS( + thread->SetPriority(nsISupportsPriority::PRIORITY_NORMAL)); + } + + if (kDEBUGThreadSleepMS) { + MOZ_ASSERT(gDEBUGThreadSlower); + + nsCOMPtr<nsIThreadInternal> thread = + do_QueryInterface(NS_GetCurrentThread()); + MOZ_ASSERT(thread); + + MOZ_ALWAYS_SUCCEEDS(thread->RemoveObserver(gDEBUGThreadSlower)); + + gDEBUGThreadSlower = nullptr; + } +#endif // DEBUG + } +} + +uint32_t +TelemetryIdForFile(nsIFile* aFile) +{ + // May be called on any thread! + + MOZ_ASSERT(aFile); + MOZ_ASSERT(gTelemetryIdMutex); + + // The storage directory is structured like this: + // + // <profile>/storage/<persistence>/<origin>/idb/<filename>.sqlite + // + // For the purposes of this function we're only concerned with the + // <persistence>, <origin>, and <filename> pieces. + + nsString filename; + MOZ_ALWAYS_SUCCEEDS(aFile->GetLeafName(filename)); + + // Make sure we were given a database file. + NS_NAMED_LITERAL_STRING(sqliteExtension, ".sqlite"); + + MOZ_ASSERT(StringEndsWith(filename, sqliteExtension)); + + filename.Truncate(filename.Length() - sqliteExtension.Length()); + + // Get the "idb" directory. + nsCOMPtr<nsIFile> idbDirectory; + MOZ_ALWAYS_SUCCEEDS(aFile->GetParent(getter_AddRefs(idbDirectory))); + + DebugOnly<nsString> idbLeafName; + MOZ_ASSERT(NS_SUCCEEDED(idbDirectory->GetLeafName(idbLeafName))); + MOZ_ASSERT(static_cast<nsString&>(idbLeafName).EqualsLiteral("idb")); + + // Get the <origin> directory. + nsCOMPtr<nsIFile> originDirectory; + MOZ_ALWAYS_SUCCEEDS( + idbDirectory->GetParent(getter_AddRefs(originDirectory))); + + nsString origin; + MOZ_ALWAYS_SUCCEEDS(originDirectory->GetLeafName(origin)); + + // Any databases in these directories are owned by the application and should + // not have their filenames masked. Hopefully they also appear in the + // Telemetry.cpp whitelist. + if (origin.EqualsLiteral("chrome") || + origin.EqualsLiteral("moz-safe-about+home")) { + return 0; + } + + // Get the <persistence> directory. + nsCOMPtr<nsIFile> persistenceDirectory; + MOZ_ALWAYS_SUCCEEDS( + originDirectory->GetParent(getter_AddRefs(persistenceDirectory))); + + nsString persistence; + MOZ_ALWAYS_SUCCEEDS(persistenceDirectory->GetLeafName(persistence)); + + NS_NAMED_LITERAL_STRING(separator, "*"); + + uint32_t hashValue = HashString(persistence + separator + + origin + separator + + filename); + + MutexAutoLock lock(*gTelemetryIdMutex); + + if (!gTelemetryIdHashtable) { + gTelemetryIdHashtable = new TelemetryIdHashtable(); + } + + uint32_t id; + if (!gTelemetryIdHashtable->Get(hashValue, &id)) { + static uint32_t sNextId = 1; + + // We're locked, no need for atomics. + id = sNextId++; + + gTelemetryIdHashtable->Put(hashValue, id); + } + + return id; +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +PBackgroundIDBFactoryParent* +AllocPBackgroundIDBFactoryParent(const LoggingInfo& aLoggingInfo) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_WARN_IF(!aLoggingInfo.nextTransactionSerialNumber()) || + NS_WARN_IF(!aLoggingInfo.nextVersionChangeTransactionSerialNumber()) || + NS_WARN_IF(!aLoggingInfo.nextRequestSerialNumber())) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr<Factory> actor = Factory::Create(aLoggingInfo); + MOZ_ASSERT(actor); + + return actor.forget().take(); +} + +bool +RecvPBackgroundIDBFactoryConstructor(PBackgroundIDBFactoryParent* aActor, + const LoggingInfo& /* aLoggingInfo */) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + return true; +} + +bool +DeallocPBackgroundIDBFactoryParent(PBackgroundIDBFactoryParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<Factory> actor = dont_AddRef(static_cast<Factory*>(aActor)); + return true; +} + +PBackgroundIndexedDBUtilsParent* +AllocPBackgroundIndexedDBUtilsParent() +{ + AssertIsOnBackgroundThread(); + + RefPtr<Utils> actor = new Utils(); + + return actor.forget().take(); +} + +bool +DeallocPBackgroundIndexedDBUtilsParent(PBackgroundIndexedDBUtilsParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<Utils> actor = dont_AddRef(static_cast<Utils*>(aActor)); + return true; +} + +bool +RecvFlushPendingFileDeletions() +{ + AssertIsOnBackgroundThread(); + + RefPtr<FlushPendingFileDeletionsRunnable> runnable = + new FlushPendingFileDeletionsRunnable(); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); + + return true; +} + +PIndexedDBPermissionRequestParent* +AllocPIndexedDBPermissionRequestParent(Element* aOwnerElement, + nsIPrincipal* aPrincipal) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<PermissionRequestHelper> actor = + new PermissionRequestHelper(aOwnerElement, aPrincipal); + return actor.forget().take(); +} + +bool +RecvPIndexedDBPermissionRequestConstructor( + PIndexedDBPermissionRequestParent* aActor) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aActor); + + auto* actor = static_cast<PermissionRequestHelper*>(aActor); + + PermissionRequestBase::PermissionValue permission; + nsresult rv = actor->PromptIfNeeded(&permission); + if (NS_FAILED(rv)) { + return false; + } + + if (permission != PermissionRequestBase::kPermissionPrompt) { + Unused << + PIndexedDBPermissionRequestParent::Send__delete__(actor, permission); + } + + return true; +} + +bool +DeallocPIndexedDBPermissionRequestParent( + PIndexedDBPermissionRequestParent* aActor) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aActor); + + RefPtr<PermissionRequestHelper> actor = + dont_AddRef(static_cast<PermissionRequestHelper*>(aActor)); + return true; +} + +already_AddRefed<mozilla::dom::quota::Client> +CreateQuotaClient() +{ + AssertIsOnBackgroundThread(); + + RefPtr<QuotaClient> client = new QuotaClient(); + return client.forget(); +} + +FileHandleThreadPool* +GetFileHandleThreadPool() +{ + AssertIsOnBackgroundThread(); + + if (!gFileHandleThreadPool) { + RefPtr<FileHandleThreadPool> fileHandleThreadPool = + FileHandleThreadPool::Create(); + if (NS_WARN_IF(!fileHandleThreadPool)) { + return nullptr; + } + + gFileHandleThreadPool = fileHandleThreadPool; + } + + return gFileHandleThreadPool; +} + +/******************************************************************************* + * DatabaseConnection implementation + ******************************************************************************/ + +DatabaseConnection::DatabaseConnection( + mozIStorageConnection* aStorageConnection, + FileManager* aFileManager) + : mStorageConnection(aStorageConnection) + , mFileManager(aFileManager) + , mInReadTransaction(false) + , mInWriteTransaction(false) +#ifdef DEBUG + , mDEBUGSavepointCount(0) + , mDEBUGThread(PR_GetCurrentThread()) +#endif +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aStorageConnection); + MOZ_ASSERT(aFileManager); +} + +DatabaseConnection::~DatabaseConnection() +{ + MOZ_ASSERT(!mStorageConnection); + MOZ_ASSERT(!mFileManager); + MOZ_ASSERT(!mCachedStatements.Count()); + MOZ_ASSERT(!mUpdateRefcountFunction); + MOZ_ASSERT(!mInWriteTransaction); + MOZ_ASSERT(!mDEBUGSavepointCount); +} + +nsresult +DatabaseConnection::Init() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + CachedStatement stmt; + nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN;"), &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInReadTransaction = true; + + return NS_OK; +} + +nsresult +DatabaseConnection::GetCachedStatement(const nsACString& aQuery, + CachedStatement* aCachedStatement) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aQuery.IsEmpty()); + MOZ_ASSERT(aCachedStatement); + MOZ_ASSERT(mStorageConnection); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::GetCachedStatement", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<mozIStorageStatement> stmt; + + if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { + nsresult rv = + mStorageConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { +#ifdef DEBUG + nsCString msg; + MOZ_ALWAYS_SUCCEEDS(mStorageConnection->GetLastErrorString(msg)); + + nsAutoCString error = + NS_LITERAL_CSTRING("The statement '") + aQuery + + NS_LITERAL_CSTRING("' failed to compile with the error message '") + + msg + NS_LITERAL_CSTRING("'."); + + NS_WARNING(error.get()); +#endif + return rv; + } + + mCachedStatements.Put(aQuery, stmt); + } + + aCachedStatement->Assign(this, stmt.forget()); + return NS_OK; +} + +nsresult +DatabaseConnection::BeginWriteTransaction() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::BeginWriteTransaction", + js::ProfileEntry::Category::STORAGE); + + // Release our read locks. + CachedStatement rollbackStmt; + nsresult rv = + GetCachedStatement(NS_LITERAL_CSTRING("ROLLBACK;"), &rollbackStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = rollbackStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInReadTransaction = false; + + if (!mUpdateRefcountFunction) { + MOZ_ASSERT(mFileManager); + + RefPtr<UpdateRefcountFunction> function = + new UpdateRefcountFunction(this, mFileManager); + + rv = + mStorageConnection->CreateFunction(NS_LITERAL_CSTRING("update_refcount"), + /* aNumArguments */ 2, + function); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mUpdateRefcountFunction.swap(function); + } + + CachedStatement beginStmt; + rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN IMMEDIATE;"), &beginStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = beginStmt->Execute(); + if (rv == NS_ERROR_STORAGE_BUSY) { + NS_WARNING("Received NS_ERROR_STORAGE_BUSY when attempting to start write " + "transaction, retrying for up to 10 seconds"); + + // Another thread must be using the database. Wait up to 10 seconds for + // that to complete. + TimeStamp start = TimeStamp::NowLoRes(); + + while (true) { + PR_Sleep(PR_MillisecondsToInterval(100)); + + rv = beginStmt->Execute(); + if (rv != NS_ERROR_STORAGE_BUSY || + TimeStamp::NowLoRes() - start > TimeDuration::FromSeconds(10)) { + break; + } + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInWriteTransaction = true; + + return NS_OK; +} + +nsresult +DatabaseConnection::CommitWriteTransaction() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::CommitWriteTransaction", + js::ProfileEntry::Category::STORAGE); + + CachedStatement stmt; + nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("COMMIT;"), &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInWriteTransaction = false; + return NS_OK; +} + +void +DatabaseConnection::RollbackWriteTransaction() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(mStorageConnection); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::RollbackWriteTransaction", + js::ProfileEntry::Category::STORAGE); + + if (!mInWriteTransaction) { + return; + } + + DatabaseConnection::CachedStatement stmt; + nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("ROLLBACK;"), &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // This may fail if SQLite already rolled back the transaction so ignore any + // errors. + Unused << stmt->Execute(); + + mInWriteTransaction = false; +} + +void +DatabaseConnection::FinishWriteTransaction() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::FinishWriteTransaction", + js::ProfileEntry::Category::STORAGE); + + if (mUpdateRefcountFunction) { + mUpdateRefcountFunction->Reset(); + } + + CachedStatement stmt; + nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN;"), &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + mInReadTransaction = true; +} + +nsresult +DatabaseConnection::StartSavepoint() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mUpdateRefcountFunction); + MOZ_ASSERT(mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::StartSavepoint", + js::ProfileEntry::Category::STORAGE); + + CachedStatement stmt; + nsresult rv = GetCachedStatement(NS_LITERAL_CSTRING(SAVEPOINT_CLAUSE), &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mUpdateRefcountFunction->StartSavepoint(); + +#ifdef DEBUG + MOZ_ASSERT(mDEBUGSavepointCount < UINT32_MAX); + mDEBUGSavepointCount++; +#endif + + return NS_OK; +} + +nsresult +DatabaseConnection::ReleaseSavepoint() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mUpdateRefcountFunction); + MOZ_ASSERT(mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::ReleaseSavepoint", + js::ProfileEntry::Category::STORAGE); + + CachedStatement stmt; + nsresult rv = GetCachedStatement( + NS_LITERAL_CSTRING("RELEASE " SAVEPOINT_CLAUSE), + &stmt); + if (NS_SUCCEEDED(rv)) { + rv = stmt->Execute(); + if (NS_SUCCEEDED(rv)) { + mUpdateRefcountFunction->ReleaseSavepoint(); + +#ifdef DEBUG + MOZ_ASSERT(mDEBUGSavepointCount); + mDEBUGSavepointCount--; +#endif + } + } + + return rv; +} + +nsresult +DatabaseConnection::RollbackSavepoint() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mUpdateRefcountFunction); + MOZ_ASSERT(mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::RollbackSavepoint", + js::ProfileEntry::Category::STORAGE); + +#ifdef DEBUG + MOZ_ASSERT(mDEBUGSavepointCount); + mDEBUGSavepointCount--; +#endif + + mUpdateRefcountFunction->RollbackSavepoint(); + + CachedStatement stmt; + nsresult rv = GetCachedStatement( + NS_LITERAL_CSTRING("ROLLBACK TO " SAVEPOINT_CLAUSE), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // This may fail if SQLite already rolled back the savepoint so ignore any + // errors. + Unused << stmt->Execute(); + + return NS_OK; +} + +nsresult +DatabaseConnection::CheckpointInternal(CheckpointMode aMode) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::CheckpointInternal", + js::ProfileEntry::Category::STORAGE); + + nsAutoCString stmtString; + stmtString.AssignLiteral("PRAGMA wal_checkpoint("); + + switch (aMode) { + case CheckpointMode::Full: + // Ensures that the database is completely checkpointed and flushed to + // disk. + stmtString.AppendLiteral("FULL"); + break; + + case CheckpointMode::Restart: + // Like Full, but also ensures that the next write will start overwriting + // the existing WAL file rather than letting the WAL file grow. + stmtString.AppendLiteral("RESTART"); + break; + + case CheckpointMode::Truncate: + // Like Restart but also truncates the existing WAL file. + stmtString.AppendLiteral("TRUNCATE"); + break; + + default: + MOZ_CRASH("Unknown CheckpointMode!"); + } + + stmtString.AppendLiteral(");"); + + CachedStatement stmt; + nsresult rv = GetCachedStatement(stmtString, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +DatabaseConnection::DoIdleProcessing(bool aNeedsCheckpoint) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::DoIdleProcessing", + js::ProfileEntry::Category::STORAGE); + + DatabaseConnection::CachedStatement freelistStmt; + uint32_t freelistCount; + nsresult rv = GetFreelistCount(freelistStmt, &freelistCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + freelistCount = 0; + } + + CachedStatement rollbackStmt; + CachedStatement beginStmt; + if (aNeedsCheckpoint || freelistCount) { + rv = GetCachedStatement(NS_LITERAL_CSTRING("ROLLBACK;"), &rollbackStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN;"), &beginStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Release the connection's normal transaction. It's possible that it could + // fail, but that isn't a problem here. + Unused << rollbackStmt->Execute(); + + mInReadTransaction = false; + } + + bool freedSomePages = false; + + if (freelistCount) { + rv = ReclaimFreePagesWhileIdle(freelistStmt, + rollbackStmt, + freelistCount, + aNeedsCheckpoint, + &freedSomePages); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ASSERT(!freedSomePages); + } + + // Make sure we didn't leave a transaction running. + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + } + + // Truncate the WAL if we were asked to or if we managed to free some space. + if (aNeedsCheckpoint || freedSomePages) { + rv = CheckpointInternal(CheckpointMode::Truncate); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + + // Finally try to restart the read transaction if we rolled it back earlier. + if (beginStmt) { + rv = beginStmt->Execute(); + if (NS_SUCCEEDED(rv)) { + mInReadTransaction = true; + } else { + NS_WARNING("Falied to restart read transaction!"); + } + } +} + +nsresult +DatabaseConnection::ReclaimFreePagesWhileIdle( + CachedStatement& aFreelistStatement, + CachedStatement& aRollbackStatement, + uint32_t aFreelistCount, + bool aNeedsCheckpoint, + bool* aFreedSomePages) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aFreelistStatement); + MOZ_ASSERT(aRollbackStatement); + MOZ_ASSERT(aFreelistCount); + MOZ_ASSERT(aFreedSomePages); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::ReclaimFreePagesWhileIdle", + js::ProfileEntry::Category::STORAGE); + + // Make sure we don't keep working if anything else needs this thread. + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + if (NS_HasPendingEvents(currentThread)) { + *aFreedSomePages = false; + return NS_OK; + } + + // Only try to free 10% at a time so that we can bail out if this connection + // suddenly becomes active or if the thread is needed otherwise. + nsAutoCString stmtString; + stmtString.AssignLiteral("PRAGMA incremental_vacuum("); + stmtString.AppendInt(std::max(uint64_t(1), uint64_t(aFreelistCount / 10))); + stmtString.AppendLiteral(");"); + + // Make all the statements we'll need up front. + CachedStatement incrementalVacuumStmt; + nsresult rv = GetCachedStatement(stmtString, &incrementalVacuumStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CachedStatement beginImmediateStmt; + rv = GetCachedStatement(NS_LITERAL_CSTRING("BEGIN IMMEDIATE;"), + &beginImmediateStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CachedStatement commitStmt; + rv = GetCachedStatement(NS_LITERAL_CSTRING("COMMIT;"), &commitStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aNeedsCheckpoint) { + // Freeing pages is a journaled operation, so it will require additional WAL + // space. However, we're idle and are about to checkpoint anyway, so doing a + // RESTART checkpoint here should allow us to reuse any existing space. + rv = CheckpointInternal(CheckpointMode::Restart); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Start the write transaction. + rv = beginImmediateStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInWriteTransaction = true; + + bool freedSomePages = false; + + while (aFreelistCount) { + if (NS_HasPendingEvents(currentThread)) { + // Something else wants to use the thread so roll back this transaction. + // It's ok if we never make progress here because the idle service should + // eventually reclaim this space. + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + break; + } + + rv = incrementalVacuumStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + freedSomePages = true; + + rv = GetFreelistCount(aFreelistStatement, &aFreelistCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + } + + if (NS_SUCCEEDED(rv) && freedSomePages) { + // Commit the write transaction. + rv = commitStmt->Execute(); + if (NS_SUCCEEDED(rv)) { + mInWriteTransaction = false; + } else { + NS_WARNING("Failed to commit!"); + } + } + + if (NS_FAILED(rv)) { + MOZ_ASSERT(mInWriteTransaction); + + // Something failed, make sure we roll everything back. + Unused << aRollbackStatement->Execute(); + + mInWriteTransaction = false; + + return rv; + } + + *aFreedSomePages = freedSomePages; + return NS_OK; +} + +nsresult +DatabaseConnection::GetFreelistCount(CachedStatement& aCachedStatement, + uint32_t* aFreelistCount) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aFreelistCount); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::GetFreelistCount", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + + if (!aCachedStatement) { + rv = GetCachedStatement(NS_LITERAL_CSTRING("PRAGMA freelist_count;"), + &aCachedStatement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = aCachedStatement->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + // Make sure this statement is reset when leaving this function since we're + // not using the normal stack-based protection of CachedStatement. + mozStorageStatementScoper scoper(aCachedStatement); + + int32_t freelistCount; + rv = aCachedStatement->GetInt32(0, &freelistCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(freelistCount >= 0); + + *aFreelistCount = uint32_t(freelistCount); + return NS_OK; +} + +void +DatabaseConnection::Close() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(!mDEBUGSavepointCount); + MOZ_ASSERT(!mInWriteTransaction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::Close", + js::ProfileEntry::Category::STORAGE); + + if (mUpdateRefcountFunction) { + MOZ_ALWAYS_SUCCEEDS( + mStorageConnection->RemoveFunction( + NS_LITERAL_CSTRING("update_refcount"))); + mUpdateRefcountFunction = nullptr; + } + + mCachedStatements.Clear(); + + MOZ_ALWAYS_SUCCEEDS(mStorageConnection->Close()); + mStorageConnection = nullptr; + + mFileManager = nullptr; +} + +nsresult +DatabaseConnection::DisableQuotaChecks() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + if (!mQuotaObject) { + MOZ_ASSERT(!mJournalQuotaObject); + + nsresult rv = mStorageConnection->GetQuotaObjects( + getter_AddRefs(mQuotaObject), + getter_AddRefs(mJournalQuotaObject)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(mQuotaObject); + MOZ_ASSERT(mJournalQuotaObject); + } + + mQuotaObject->DisableQuotaCheck(); + mJournalQuotaObject->DisableQuotaCheck(); + + return NS_OK; +} + +void +DatabaseConnection::EnableQuotaChecks() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mQuotaObject); + MOZ_ASSERT(mJournalQuotaObject); + + RefPtr<QuotaObject> quotaObject; + RefPtr<QuotaObject> journalQuotaObject; + + mQuotaObject.swap(quotaObject); + mJournalQuotaObject.swap(journalQuotaObject); + + quotaObject->EnableQuotaCheck(); + journalQuotaObject->EnableQuotaCheck(); + + int64_t fileSize; + nsresult rv = GetFileSize(quotaObject->Path(), &fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + int64_t journalFileSize; + rv = GetFileSize(journalQuotaObject->Path(), &journalFileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + DebugOnly<bool> result = + journalQuotaObject->MaybeUpdateSize(journalFileSize, /* aTruncate */ true); + MOZ_ASSERT(result); + + result = quotaObject->MaybeUpdateSize(fileSize, /* aTruncate */ true); + MOZ_ASSERT(result); +} + +nsresult +DatabaseConnection::GetFileSize(const nsAString& aPath, int64_t* aResult) +{ + MOZ_ASSERT(!aPath.IsEmpty()); + MOZ_ASSERT(aResult); + + nsresult rv; + nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->InitWithPath(aPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t fileSize; + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + rv = file->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + fileSize = 0; + } + + *aResult = fileSize; + return NS_OK; +} + +DatabaseConnection:: +CachedStatement::CachedStatement() +#ifdef DEBUG + : mDEBUGConnection(nullptr) +#endif +{ + AssertIsOnConnectionThread(); + + MOZ_COUNT_CTOR(DatabaseConnection::CachedStatement); +} + +DatabaseConnection:: +CachedStatement::~CachedStatement() +{ + AssertIsOnConnectionThread(); + + MOZ_COUNT_DTOR(DatabaseConnection::CachedStatement); +} + +DatabaseConnection:: +CachedStatement::operator mozIStorageStatement*() const +{ + AssertIsOnConnectionThread(); + + return mStatement; +} + +mozIStorageStatement* +DatabaseConnection:: +CachedStatement::operator->() const +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStatement); + + return mStatement; +} + +void +DatabaseConnection:: +CachedStatement::Reset() +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(mStatement, mScoper); + + if (mStatement) { + mScoper.reset(); + mScoper.emplace(mStatement); + } +} + +void +DatabaseConnection:: +CachedStatement::Assign(DatabaseConnection* aConnection, + already_AddRefed<mozIStorageStatement> aStatement) +{ +#ifdef DEBUG + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(mDEBUGConnection, mDEBUGConnection == aConnection); + + mDEBUGConnection = aConnection; +#endif + AssertIsOnConnectionThread(); + + mScoper.reset(); + + mStatement = aStatement; + + if (mStatement) { + mScoper.emplace(mStatement); + } +} + +DatabaseConnection:: +AutoSavepoint::AutoSavepoint() + : mConnection(nullptr) +#ifdef DEBUG + , mDEBUGTransaction(nullptr) +#endif +{ + MOZ_COUNT_CTOR(DatabaseConnection::AutoSavepoint); +} + +DatabaseConnection:: +AutoSavepoint::~AutoSavepoint() +{ + MOZ_COUNT_DTOR(DatabaseConnection::AutoSavepoint); + + if (mConnection) { + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mDEBUGTransaction); + MOZ_ASSERT(mDEBUGTransaction->GetMode() == IDBTransaction::READ_WRITE || + mDEBUGTransaction->GetMode() == + IDBTransaction::READ_WRITE_FLUSH || + mDEBUGTransaction->GetMode() == IDBTransaction::CLEANUP || + mDEBUGTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); + + if (NS_FAILED(mConnection->RollbackSavepoint())) { + NS_WARNING("Failed to rollback savepoint!"); + } + } +} + +nsresult +DatabaseConnection:: +AutoSavepoint::Start(const TransactionBase* aTransaction) +{ + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(aTransaction->GetMode() == IDBTransaction::READ_WRITE || + aTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || + aTransaction->GetMode() == IDBTransaction::CLEANUP || + aTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); + + DatabaseConnection* connection = aTransaction->GetDatabase()->GetConnection(); + MOZ_ASSERT(connection); + connection->AssertIsOnConnectionThread(); + + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(!mDEBUGTransaction); + + nsresult rv = connection->StartSavepoint(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mConnection = connection; +#ifdef DEBUG + mDEBUGTransaction = aTransaction; +#endif + + return NS_OK; +} + +nsresult +DatabaseConnection:: +AutoSavepoint::Commit() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mDEBUGTransaction); + + nsresult rv = mConnection->ReleaseSavepoint(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mConnection = nullptr; +#ifdef DEBUG + mDEBUGTransaction = nullptr; +#endif + + return NS_OK; +} + +DatabaseConnection:: +UpdateRefcountFunction::UpdateRefcountFunction(DatabaseConnection* aConnection, + FileManager* aFileManager) + : mConnection(aConnection) + , mFileManager(aFileManager) + , mInSavepoint(false) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aFileManager); +} + +nsresult +DatabaseConnection:: +UpdateRefcountFunction::WillCommit() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::WillCommit", + js::ProfileEntry::Category::STORAGE); + + DatabaseUpdateFunction function(this); + for (auto iter = mFileInfoEntries.ConstIter(); !iter.Done(); iter.Next()) { + auto key = iter.Key(); + FileInfoEntry* value = iter.Data(); + MOZ_ASSERT(value); + + if (value->mDelta && !function.Update(key, value->mDelta)) { + break; + } + } + + nsresult rv = function.ErrorCode(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = CreateJournals(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +DatabaseConnection:: +UpdateRefcountFunction::DidCommit() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::DidCommit", + js::ProfileEntry::Category::STORAGE); + + for (auto iter = mFileInfoEntries.ConstIter(); !iter.Done(); iter.Next()) { + FileInfoEntry* value = iter.Data(); + + MOZ_ASSERT(value); + + if (value->mDelta) { + value->mFileInfo->UpdateDBRefs(value->mDelta); + } + } + + if (NS_FAILED(RemoveJournals(mJournalsToRemoveAfterCommit))) { + NS_WARNING("RemoveJournals failed!"); + } +} + +void +DatabaseConnection:: +UpdateRefcountFunction::DidAbort() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::DidAbort", + js::ProfileEntry::Category::STORAGE); + + if (NS_FAILED(RemoveJournals(mJournalsToRemoveAfterAbort))) { + NS_WARNING("RemoveJournals failed!"); + } +} + +void +DatabaseConnection:: +UpdateRefcountFunction::StartSavepoint() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!mInSavepoint); + MOZ_ASSERT(!mSavepointEntriesIndex.Count()); + + mInSavepoint = true; +} + +void +DatabaseConnection:: +UpdateRefcountFunction::ReleaseSavepoint() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mInSavepoint); + + mSavepointEntriesIndex.Clear(); + mInSavepoint = false; +} + +void +DatabaseConnection:: +UpdateRefcountFunction::RollbackSavepoint() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mInSavepoint); + + for (auto iter = mSavepointEntriesIndex.ConstIter(); + !iter.Done(); iter.Next()) { + auto value = iter.Data(); + value->mDelta -= value->mSavepointDelta; + } + + mInSavepoint = false; + mSavepointEntriesIndex.Clear(); +} + +void +DatabaseConnection:: +UpdateRefcountFunction::Reset() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!mSavepointEntriesIndex.Count()); + MOZ_ASSERT(!mInSavepoint); + + class MOZ_STACK_CLASS CustomCleanupCallback final + : public FileInfo::CustomCleanupCallback + { + nsCOMPtr<nsIFile> mDirectory; + nsCOMPtr<nsIFile> mJournalDirectory; + + public: + virtual nsresult + Cleanup(FileManager* aFileManager, int64_t aId) + { + if (!mDirectory) { + MOZ_ASSERT(!mJournalDirectory); + + mDirectory = aFileManager->GetDirectory(); + if (NS_WARN_IF(!mDirectory)) { + return NS_ERROR_FAILURE; + } + + mJournalDirectory = aFileManager->GetJournalDirectory(); + if (NS_WARN_IF(!mJournalDirectory)) { + return NS_ERROR_FAILURE; + } + } + + nsCOMPtr<nsIFile> file = aFileManager->GetFileForId(mDirectory, aId); + if (NS_WARN_IF(!file)) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + int64_t fileSize; + + if (aFileManager->EnforcingQuota()) { + rv = file->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = file->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aFileManager->EnforcingQuota()) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->DecreaseUsageForOrigin(aFileManager->Type(), + aFileManager->Group(), + aFileManager->Origin(), + fileSize); + } + + file = aFileManager->GetFileForId(mJournalDirectory, aId); + if (NS_WARN_IF(!file)) { + return NS_ERROR_FAILURE; + } + + rv = file->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + }; + + mJournalsToCreateBeforeCommit.Clear(); + mJournalsToRemoveAfterCommit.Clear(); + mJournalsToRemoveAfterAbort.Clear(); + + // FileInfo implementation automatically removes unreferenced files, but it's + // done asynchronously and with a delay. We want to remove them (and decrease + // quota usage) before we fire the commit event. + for (auto iter = mFileInfoEntries.ConstIter(); !iter.Done(); iter.Next()) { + FileInfoEntry* value = iter.Data(); + + MOZ_ASSERT(value); + + FileInfo* fileInfo = value->mFileInfo.forget().take(); + + MOZ_ASSERT(fileInfo); + + CustomCleanupCallback customCleanupCallback; + fileInfo->Release(&customCleanupCallback); + } + + mFileInfoEntries.Clear(); +} + +nsresult +DatabaseConnection:: +UpdateRefcountFunction::ProcessValue(mozIStorageValueArray* aValues, + int32_t aIndex, + UpdateType aUpdateType) +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aValues); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::ProcessValue", + js::ProfileEntry::Category::STORAGE); + + int32_t type; + nsresult rv = aValues->GetTypeOfIndex(aIndex, &type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (type == mozIStorageValueArray::VALUE_TYPE_NULL) { + return NS_OK; + } + + nsString ids; + rv = aValues->GetString(aIndex, ids); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsTArray<StructuredCloneFile> files; + rv = DeserializeStructuredCloneFiles(mFileManager, ids, files, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (uint32_t i = 0; i < files.Length(); i++) { + const StructuredCloneFile& file = files[i]; + + const int64_t id = file.mFileInfo->Id(); + MOZ_ASSERT(id > 0); + + FileInfoEntry* entry; + if (!mFileInfoEntries.Get(id, &entry)) { + entry = new FileInfoEntry(file.mFileInfo); + mFileInfoEntries.Put(id, entry); + } + + if (mInSavepoint) { + mSavepointEntriesIndex.Put(id, entry); + } + + switch (aUpdateType) { + case UpdateType::Increment: + entry->mDelta++; + if (mInSavepoint) { + entry->mSavepointDelta++; + } + break; + case UpdateType::Decrement: + entry->mDelta--; + if (mInSavepoint) { + entry->mSavepointDelta--; + } + break; + default: + MOZ_CRASH("Unknown update type!"); + } + } + + return NS_OK; +} + +nsresult +DatabaseConnection:: +UpdateRefcountFunction::CreateJournals() +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::CreateJournals", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<nsIFile> journalDirectory = mFileManager->GetJournalDirectory(); + if (NS_WARN_IF(!journalDirectory)) { + return NS_ERROR_FAILURE; + } + + for (uint32_t i = 0; i < mJournalsToCreateBeforeCommit.Length(); i++) { + int64_t id = mJournalsToCreateBeforeCommit[i]; + + nsCOMPtr<nsIFile> file = + mFileManager->GetFileForId(journalDirectory, id); + if (NS_WARN_IF(!file)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mJournalsToRemoveAfterAbort.AppendElement(id); + } + + return NS_OK; +} + +nsresult +DatabaseConnection:: +UpdateRefcountFunction::RemoveJournals(const nsTArray<int64_t>& aJournals) +{ + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::RemoveJournals", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<nsIFile> journalDirectory = mFileManager->GetJournalDirectory(); + if (NS_WARN_IF(!journalDirectory)) { + return NS_ERROR_FAILURE; + } + + for (uint32_t index = 0; index < aJournals.Length(); index++) { + nsCOMPtr<nsIFile> file = + mFileManager->GetFileForId(journalDirectory, aJournals[index]); + if (NS_WARN_IF(!file)) { + return NS_ERROR_FAILURE; + } + + if (NS_FAILED(file->Remove(false))) { + NS_WARNING("Failed to removed journal!"); + } + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(DatabaseConnection::UpdateRefcountFunction, + mozIStorageFunction) + +NS_IMETHODIMP +DatabaseConnection:: +UpdateRefcountFunction::OnFunctionCall(mozIStorageValueArray* aValues, + nsIVariant** _retval) +{ + MOZ_ASSERT(aValues); + MOZ_ASSERT(_retval); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::OnFunctionCall", + js::ProfileEntry::Category::STORAGE); + + uint32_t numEntries; + nsresult rv = aValues->GetNumEntries(&numEntries); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(numEntries == 2); + +#ifdef DEBUG + { + int32_t type1 = mozIStorageValueArray::VALUE_TYPE_NULL; + MOZ_ASSERT(NS_SUCCEEDED(aValues->GetTypeOfIndex(0, &type1))); + + int32_t type2 = mozIStorageValueArray::VALUE_TYPE_NULL; + MOZ_ASSERT(NS_SUCCEEDED(aValues->GetTypeOfIndex(1, &type2))); + + MOZ_ASSERT(!(type1 == mozIStorageValueArray::VALUE_TYPE_NULL && + type2 == mozIStorageValueArray::VALUE_TYPE_NULL)); + } +#endif + + rv = ProcessValue(aValues, 0, UpdateType::Decrement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = ProcessValue(aValues, 1, UpdateType::Increment); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool +DatabaseConnection::UpdateRefcountFunction:: +DatabaseUpdateFunction::Update(int64_t aId, + int32_t aDelta) +{ + nsresult rv = UpdateInternal(aId, aDelta); + if (NS_FAILED(rv)) { + mErrorCode = rv; + return false; + } + + return true; +} + +nsresult +DatabaseConnection::UpdateRefcountFunction:: +DatabaseUpdateFunction::UpdateInternal(int64_t aId, + int32_t aDelta) +{ + MOZ_ASSERT(mFunction); + + PROFILER_LABEL("IndexedDB", + "DatabaseConnection::UpdateRefcountFunction::" + "DatabaseUpdateFunction::UpdateInternal", + js::ProfileEntry::Category::STORAGE); + + DatabaseConnection* connection = mFunction->mConnection; + MOZ_ASSERT(connection); + connection->AssertIsOnConnectionThread(); + + MOZ_ASSERT(connection->GetStorageConnection()); + + nsresult rv; + if (!mUpdateStatement) { + rv = connection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE file " + "SET refcount = refcount + :delta " + "WHERE id = :id"), + &mUpdateStatement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mozStorageStatementScoper updateScoper(mUpdateStatement); + + rv = mUpdateStatement->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mUpdateStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mUpdateStatement->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t rows; + rv = connection->GetStorageConnection()->GetAffectedRows(&rows); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (rows > 0) { + if (!mSelectStatement) { + rv = connection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT id " + "FROM file " + "WHERE id = :id"), + &mSelectStatement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mozStorageStatementScoper selectScoper(mSelectStatement); + + rv = mSelectStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = mSelectStatement->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!hasResult) { + // Don't have to create the journal here, we can create all at once, + // just before commit + mFunction->mJournalsToCreateBeforeCommit.AppendElement(aId); + } + + return NS_OK; + } + + if (!mInsertStatement) { + rv = connection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT INTO file (id, refcount) " + "VALUES(:id, :delta)"), + &mInsertStatement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mozStorageStatementScoper insertScoper(mInsertStatement); + + rv = mInsertStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mInsertStatement->BindInt32ByName(NS_LITERAL_CSTRING("delta"), aDelta); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mInsertStatement->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mFunction->mJournalsToRemoveAfterCommit.AppendElement(aId); + return NS_OK; +} + +/******************************************************************************* + * ConnectionPool implementation + ******************************************************************************/ + +ConnectionPool::ConnectionPool() + : mDatabasesMutex("ConnectionPool::mDatabasesMutex") + , mIdleTimer(do_CreateInstance(NS_TIMER_CONTRACTID)) + , mNextTransactionId(0) + , mTotalThreadCount(0) + , mShutdownRequested(false) + , mShutdownComplete(false) +#ifdef DEBUG + , mDEBUGOwningThread(PR_GetCurrentThread()) +#endif +{ + AssertIsOnOwningThread(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mIdleTimer); +} + +ConnectionPool::~ConnectionPool() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mIdleThreads.IsEmpty()); + MOZ_ASSERT(mIdleDatabases.IsEmpty()); + MOZ_ASSERT(!mIdleTimer); + MOZ_ASSERT(mTargetIdleTime.IsNull()); + MOZ_ASSERT(!mDatabases.Count()); + MOZ_ASSERT(!mTransactions.Count()); + MOZ_ASSERT(mQueuedTransactions.IsEmpty()); + MOZ_ASSERT(mCompleteCallbacks.IsEmpty()); + MOZ_ASSERT(!mTotalThreadCount); + MOZ_ASSERT(mShutdownRequested); + MOZ_ASSERT(mShutdownComplete); +} + +#ifdef DEBUG + +void +ConnectionPool::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mDEBUGOwningThread); + MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGOwningThread); +} + +#endif // DEBUG + +// static +void +ConnectionPool::IdleTimerCallback(nsITimer* aTimer, void* aClosure) +{ + MOZ_ASSERT(aTimer); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::IdleTimerCallback", + js::ProfileEntry::Category::STORAGE); + + auto* self = static_cast<ConnectionPool*>(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mIdleTimer); + MOZ_ASSERT(SameCOMIdentity(self->mIdleTimer, aTimer)); + MOZ_ASSERT(!self->mTargetIdleTime.IsNull()); + MOZ_ASSERT_IF(self->mIdleDatabases.IsEmpty(), !self->mIdleThreads.IsEmpty()); + MOZ_ASSERT_IF(self->mIdleThreads.IsEmpty(), !self->mIdleDatabases.IsEmpty()); + + self->mTargetIdleTime = TimeStamp(); + + // Cheat a little. + TimeStamp now = TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(500); + + uint32_t index = 0; + + for (uint32_t count = self->mIdleDatabases.Length(); index < count; index++) { + IdleDatabaseInfo& info = self->mIdleDatabases[index]; + + if (now >= info.mIdleTime) { + if (info.mDatabaseInfo->mIdle) { + self->PerformIdleDatabaseMaintenance(info.mDatabaseInfo); + } else { + self->CloseDatabase(info.mDatabaseInfo); + } + } else { + break; + } + } + + if (index) { + self->mIdleDatabases.RemoveElementsAt(0, index); + + index = 0; + } + + for (uint32_t count = self->mIdleThreads.Length(); index < count; index++) { + IdleThreadInfo& info = self->mIdleThreads[index]; + MOZ_ASSERT(info.mThreadInfo.mThread); + MOZ_ASSERT(info.mThreadInfo.mRunnable); + + if (now >= info.mIdleTime) { + self->ShutdownThread(info.mThreadInfo); + } else { + break; + } + } + + if (index) { + self->mIdleThreads.RemoveElementsAt(0, index); + } + + self->AdjustIdleTimer(); +} + +nsresult +ConnectionPool::GetOrCreateConnection(const Database* aDatabase, + DatabaseConnection** aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aDatabase); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::GetOrCreateConnection", + js::ProfileEntry::Category::STORAGE); + + DatabaseInfo* dbInfo; + { + MutexAutoLock lock(mDatabasesMutex); + + dbInfo = mDatabases.Get(aDatabase->Id()); + } + + MOZ_ASSERT(dbInfo); + + RefPtr<DatabaseConnection> connection = dbInfo->mConnection; + if (!connection) { + MOZ_ASSERT(!dbInfo->mDEBUGConnectionThread); + + nsCOMPtr<mozIStorageConnection> storageConnection; + nsresult rv = + GetStorageConnection(aDatabase->FilePath(), + aDatabase->Type(), + aDatabase->Group(), + aDatabase->Origin(), + aDatabase->TelemetryId(), + getter_AddRefs(storageConnection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + connection = + new DatabaseConnection(storageConnection, aDatabase->GetFileManager()); + + rv = connection->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + dbInfo->mConnection = connection; + + IDB_DEBUG_LOG(("ConnectionPool created connection 0x%p for '%s'", + dbInfo->mConnection.get(), + NS_ConvertUTF16toUTF8(aDatabase->FilePath()).get())); + +#ifdef DEBUG + dbInfo->mDEBUGConnectionThread = PR_GetCurrentThread(); +#endif + } + + dbInfo->AssertIsOnConnectionThread(); + + connection.forget(aConnection); + return NS_OK; +} + +uint64_t +ConnectionPool::Start(const nsID& aBackgroundChildLoggingId, + const nsACString& aDatabaseId, + int64_t aLoggingSerialNumber, + const nsTArray<nsString>& aObjectStoreNames, + bool aIsWriteTransaction, + TransactionDatabaseOperationBase* aTransactionOp) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseId.IsEmpty()); + MOZ_ASSERT(mNextTransactionId < UINT64_MAX); + MOZ_ASSERT(!mShutdownRequested); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::Start", + js::ProfileEntry::Category::STORAGE); + + const uint64_t transactionId = ++mNextTransactionId; + + DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId); + + const bool databaseInfoIsNew = !dbInfo; + + if (databaseInfoIsNew) { + dbInfo = new DatabaseInfo(this, aDatabaseId); + + MutexAutoLock lock(mDatabasesMutex); + + mDatabases.Put(aDatabaseId, dbInfo); + } + + TransactionInfo* transactionInfo = + new TransactionInfo(dbInfo, + aBackgroundChildLoggingId, + aDatabaseId, + transactionId, + aLoggingSerialNumber, + aObjectStoreNames, + aIsWriteTransaction, + aTransactionOp); + + MOZ_ASSERT(!mTransactions.Get(transactionId)); + mTransactions.Put(transactionId, transactionInfo); + + if (aIsWriteTransaction) { + MOZ_ASSERT(dbInfo->mWriteTransactionCount < UINT32_MAX); + dbInfo->mWriteTransactionCount++; + } else { + MOZ_ASSERT(dbInfo->mReadTransactionCount < UINT32_MAX); + dbInfo->mReadTransactionCount++; + } + + auto& blockingTransactions = dbInfo->mBlockingTransactions; + + for (uint32_t nameIndex = 0, nameCount = aObjectStoreNames.Length(); + nameIndex < nameCount; + nameIndex++) { + const nsString& objectStoreName = aObjectStoreNames[nameIndex]; + + TransactionInfoPair* blockInfo = blockingTransactions.Get(objectStoreName); + if (!blockInfo) { + blockInfo = new TransactionInfoPair(); + blockingTransactions.Put(objectStoreName, blockInfo); + } + + // Mark what we are blocking on. + if (TransactionInfo* blockingRead = blockInfo->mLastBlockingReads) { + transactionInfo->mBlockedOn.PutEntry(blockingRead); + blockingRead->AddBlockingTransaction(transactionInfo); + } + + if (aIsWriteTransaction) { + if (const uint32_t writeCount = blockInfo->mLastBlockingWrites.Length()) { + for (uint32_t writeIndex = 0; writeIndex < writeCount; writeIndex++) { + TransactionInfo* blockingWrite = + blockInfo->mLastBlockingWrites[writeIndex]; + MOZ_ASSERT(blockingWrite); + + transactionInfo->mBlockedOn.PutEntry(blockingWrite); + blockingWrite->AddBlockingTransaction(transactionInfo); + } + } + + blockInfo->mLastBlockingReads = transactionInfo; + blockInfo->mLastBlockingWrites.Clear(); + } else { + blockInfo->mLastBlockingWrites.AppendElement(transactionInfo); + } + } + + if (!transactionInfo->mBlockedOn.Count()) { + Unused << ScheduleTransaction(transactionInfo, + /* aFromQueuedTransactions */ false); + } + + if (!databaseInfoIsNew && + (mIdleDatabases.RemoveElement(dbInfo) || + mDatabasesPerformingIdleMaintenance.RemoveElement(dbInfo))) { + AdjustIdleTimer(); + } + + return transactionId; +} + +void +ConnectionPool::Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aRunnable); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::Dispatch", + js::ProfileEntry::Category::STORAGE); + + TransactionInfo* transactionInfo = mTransactions.Get(aTransactionId); + MOZ_ASSERT(transactionInfo); + MOZ_ASSERT(!transactionInfo->mFinished); + + if (transactionInfo->mRunning) { + DatabaseInfo* dbInfo = transactionInfo->mDatabaseInfo; + MOZ_ASSERT(dbInfo); + MOZ_ASSERT(dbInfo->mThreadInfo.mThread); + MOZ_ASSERT(dbInfo->mThreadInfo.mRunnable); + MOZ_ASSERT(!dbInfo->mClosing); + MOZ_ASSERT_IF(transactionInfo->mIsWriteTransaction, + dbInfo->mRunningWriteTransaction == transactionInfo); + + MOZ_ALWAYS_SUCCEEDS( + dbInfo->mThreadInfo.mThread->Dispatch(aRunnable, NS_DISPATCH_NORMAL)); + } else { + transactionInfo->mQueuedRunnables.AppendElement(aRunnable); + } +} + +void +ConnectionPool::Finish(uint64_t aTransactionId, FinishCallback* aCallback) +{ + AssertIsOnOwningThread(); + +#ifdef DEBUG + TransactionInfo* transactionInfo = mTransactions.Get(aTransactionId); + MOZ_ASSERT(transactionInfo); + MOZ_ASSERT(!transactionInfo->mFinished); +#endif + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::Finish", + js::ProfileEntry::Category::STORAGE); + + RefPtr<FinishCallbackWrapper> wrapper = + new FinishCallbackWrapper(this, aTransactionId, aCallback); + + Dispatch(aTransactionId, wrapper); + +#ifdef DEBUG + MOZ_ASSERT(!transactionInfo->mFinished); + transactionInfo->mFinished = true; +#endif +} + +void +ConnectionPool::WaitForDatabasesToComplete(nsTArray<nsCString>&& aDatabaseIds, + nsIRunnable* aCallback) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseIds.IsEmpty()); + MOZ_ASSERT(aCallback); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::WaitForDatabasesToComplete", + js::ProfileEntry::Category::STORAGE); + + bool mayRunCallbackImmediately = true; + + for (uint32_t index = 0, count = aDatabaseIds.Length(); + index < count; + index++) { + const nsCString& databaseId = aDatabaseIds[index]; + MOZ_ASSERT(!databaseId.IsEmpty()); + + if (CloseDatabaseWhenIdleInternal(databaseId)) { + mayRunCallbackImmediately = false; + } + } + + if (mayRunCallbackImmediately) { + Unused << aCallback->Run(); + return; + } + + nsAutoPtr<DatabasesCompleteCallback> callback( + new DatabasesCompleteCallback(Move(aDatabaseIds), aCallback)); + mCompleteCallbacks.AppendElement(callback.forget()); +} + +void +ConnectionPool::Shutdown() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mShutdownRequested); + MOZ_ASSERT(!mShutdownComplete); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::Shutdown", + js::ProfileEntry::Category::STORAGE); + + mShutdownRequested = true; + + CancelIdleTimer(); + MOZ_ASSERT(mTargetIdleTime.IsNull()); + + mIdleTimer = nullptr; + + CloseIdleDatabases(); + + ShutdownIdleThreads(); + + if (!mDatabases.Count()) { + MOZ_ASSERT(!mTransactions.Count()); + + Cleanup(); + + MOZ_ASSERT(mShutdownComplete); + return; + } + + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + while (!mShutdownComplete) { + MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread)); + } +} + +void +ConnectionPool::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mShutdownRequested); + MOZ_ASSERT(!mShutdownComplete); + MOZ_ASSERT(!mDatabases.Count()); + MOZ_ASSERT(!mTransactions.Count()); + MOZ_ASSERT(mIdleThreads.IsEmpty()); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::Cleanup", + js::ProfileEntry::Category::STORAGE); + + if (!mCompleteCallbacks.IsEmpty()) { + // Run all callbacks manually now. + for (uint32_t count = mCompleteCallbacks.Length(), index = 0; + index < count; + index++) { + nsAutoPtr<DatabasesCompleteCallback> completeCallback( + mCompleteCallbacks[index].forget()); + MOZ_ASSERT(completeCallback); + MOZ_ASSERT(completeCallback->mCallback); + + Unused << completeCallback->mCallback->Run(); + } + + mCompleteCallbacks.Clear(); + + // And make sure they get processed. + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + MOZ_ALWAYS_SUCCEEDS(NS_ProcessPendingEvents(currentThread)); + } + + mShutdownComplete = true; +} + +void +ConnectionPool::AdjustIdleTimer() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mIdleTimer); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::AdjustIdleTimer", + js::ProfileEntry::Category::STORAGE); + + // Figure out the next time at which we should release idle resources. This + // includes both databases and threads. + TimeStamp newTargetIdleTime; + MOZ_ASSERT(newTargetIdleTime.IsNull()); + + if (!mIdleDatabases.IsEmpty()) { + newTargetIdleTime = mIdleDatabases[0].mIdleTime; + } + + if (!mIdleThreads.IsEmpty()) { + const TimeStamp& idleTime = mIdleThreads[0].mIdleTime; + + if (newTargetIdleTime.IsNull() || idleTime < newTargetIdleTime) { + newTargetIdleTime = idleTime; + } + } + + MOZ_ASSERT_IF(newTargetIdleTime.IsNull(), mIdleDatabases.IsEmpty()); + MOZ_ASSERT_IF(newTargetIdleTime.IsNull(), mIdleThreads.IsEmpty()); + + // Cancel the timer if it was running and the new target time is different. + if (!mTargetIdleTime.IsNull() && + (newTargetIdleTime.IsNull() || mTargetIdleTime != newTargetIdleTime)) { + CancelIdleTimer(); + + MOZ_ASSERT(mTargetIdleTime.IsNull()); + } + + // Schedule the timer if we have a target time different than before. + if (!newTargetIdleTime.IsNull() && + (mTargetIdleTime.IsNull() || mTargetIdleTime != newTargetIdleTime)) { + double delta = (newTargetIdleTime - TimeStamp::NowLoRes()).ToMilliseconds(); + + uint32_t delay; + if (delta > 0) { + delay = uint32_t(std::min(delta, double(UINT32_MAX))); + } else { + delay = 0; + } + + MOZ_ALWAYS_SUCCEEDS( + mIdleTimer->InitWithFuncCallback(IdleTimerCallback, + this, + delay, + nsITimer::TYPE_ONE_SHOT)); + + mTargetIdleTime = newTargetIdleTime; + } +} + +void +ConnectionPool::CancelIdleTimer() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mIdleTimer); + + if (!mTargetIdleTime.IsNull()) { + MOZ_ALWAYS_SUCCEEDS(mIdleTimer->Cancel()); + + mTargetIdleTime = TimeStamp(); + MOZ_ASSERT(mTargetIdleTime.IsNull()); + } +} + +void +ConnectionPool::ShutdownThread(ThreadInfo& aThreadInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aThreadInfo.mThread); + MOZ_ASSERT(aThreadInfo.mRunnable); + MOZ_ASSERT(mTotalThreadCount); + + RefPtr<ThreadRunnable> runnable; + aThreadInfo.mRunnable.swap(runnable); + + nsCOMPtr<nsIThread> thread; + aThreadInfo.mThread.swap(thread); + + IDB_DEBUG_LOG(("ConnectionPool shutting down thread %lu", + runnable->SerialNumber())); + + // This should clean up the thread with the profiler. + MOZ_ALWAYS_SUCCEEDS(thread->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL)); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread( + NewRunnableMethod(thread, &nsIThread::Shutdown))); + + mTotalThreadCount--; +} + +void +ConnectionPool::CloseIdleDatabases() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mShutdownRequested); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::CloseIdleDatabases", + js::ProfileEntry::Category::STORAGE); + + if (!mIdleDatabases.IsEmpty()) { + for (IdleDatabaseInfo& idleInfo : mIdleDatabases) { + CloseDatabase(idleInfo.mDatabaseInfo); + } + mIdleDatabases.Clear(); + } + + if (!mDatabasesPerformingIdleMaintenance.IsEmpty()) { + for (DatabaseInfo* dbInfo : mDatabasesPerformingIdleMaintenance) { + MOZ_ASSERT(dbInfo); + CloseDatabase(dbInfo); + } + mDatabasesPerformingIdleMaintenance.Clear(); + } +} + +void +ConnectionPool::ShutdownIdleThreads() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mShutdownRequested); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::ShutdownIdleThreads", + js::ProfileEntry::Category::STORAGE); + + if (!mIdleThreads.IsEmpty()) { + for (uint32_t threadCount = mIdleThreads.Length(), threadIndex = 0; + threadIndex < threadCount; + threadIndex++) { + ShutdownThread(mIdleThreads[threadIndex].mThreadInfo); + } + mIdleThreads.Clear(); + } +} + +bool +ConnectionPool::ScheduleTransaction(TransactionInfo* aTransactionInfo, + bool aFromQueuedTransactions) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aTransactionInfo); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::ScheduleTransaction", + js::ProfileEntry::Category::STORAGE); + + DatabaseInfo* dbInfo = aTransactionInfo->mDatabaseInfo; + MOZ_ASSERT(dbInfo); + + dbInfo->mIdle = false; + + if (dbInfo->mClosing) { + MOZ_ASSERT(!mIdleDatabases.Contains(dbInfo)); + MOZ_ASSERT( + !dbInfo->mTransactionsScheduledDuringClose.Contains(aTransactionInfo)); + + dbInfo->mTransactionsScheduledDuringClose.AppendElement(aTransactionInfo); + return true; + } + + if (!dbInfo->mThreadInfo.mThread) { + MOZ_ASSERT(!dbInfo->mThreadInfo.mRunnable); + + if (mIdleThreads.IsEmpty()) { + bool created = false; + + if (mTotalThreadCount < kMaxConnectionThreadCount) { + // This will set the thread up with the profiler. + RefPtr<ThreadRunnable> runnable = new ThreadRunnable(); + + nsCOMPtr<nsIThread> newThread; + if (NS_SUCCEEDED(NS_NewThread(getter_AddRefs(newThread), runnable))) { + MOZ_ASSERT(newThread); + + IDB_DEBUG_LOG(("ConnectionPool created thread %lu", + runnable->SerialNumber())); + + dbInfo->mThreadInfo.mThread.swap(newThread); + dbInfo->mThreadInfo.mRunnable.swap(runnable); + + mTotalThreadCount++; + created = true; + } else { + NS_WARNING("Failed to make new thread!"); + } + } else if (!mDatabasesPerformingIdleMaintenance.IsEmpty()) { + // We need a thread right now so force all idle processing to stop by + // posting a dummy runnable to each thread that might be doing idle + // maintenance. + nsCOMPtr<nsIRunnable> runnable = new Runnable(); + + for (uint32_t index = mDatabasesPerformingIdleMaintenance.Length(); + index > 0; + index--) { + DatabaseInfo* dbInfo = mDatabasesPerformingIdleMaintenance[index - 1]; + MOZ_ASSERT(dbInfo); + MOZ_ASSERT(dbInfo->mThreadInfo.mThread); + + MOZ_ALWAYS_SUCCEEDS( + dbInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL)); + } + } + + if (!created) { + if (!aFromQueuedTransactions) { + MOZ_ASSERT(!mQueuedTransactions.Contains(aTransactionInfo)); + mQueuedTransactions.AppendElement(aTransactionInfo); + } + return false; + } + } else { + const uint32_t lastIndex = mIdleThreads.Length() - 1; + + ThreadInfo& threadInfo = mIdleThreads[lastIndex].mThreadInfo; + + dbInfo->mThreadInfo.mRunnable.swap(threadInfo.mRunnable); + dbInfo->mThreadInfo.mThread.swap(threadInfo.mThread); + + mIdleThreads.RemoveElementAt(lastIndex); + + AdjustIdleTimer(); + } + } + + MOZ_ASSERT(dbInfo->mThreadInfo.mThread); + MOZ_ASSERT(dbInfo->mThreadInfo.mRunnable); + + if (aTransactionInfo->mIsWriteTransaction) { + if (dbInfo->mRunningWriteTransaction) { + // SQLite only allows one write transaction at a time so queue this + // transaction for later. + MOZ_ASSERT( + !dbInfo->mScheduledWriteTransactions.Contains(aTransactionInfo)); + + dbInfo->mScheduledWriteTransactions.AppendElement(aTransactionInfo); + return true; + } + + dbInfo->mRunningWriteTransaction = aTransactionInfo; + dbInfo->mNeedsCheckpoint = true; + } + + MOZ_ASSERT(!aTransactionInfo->mRunning); + aTransactionInfo->mRunning = true; + + nsTArray<nsCOMPtr<nsIRunnable>>& queuedRunnables = + aTransactionInfo->mQueuedRunnables; + + if (!queuedRunnables.IsEmpty()) { + for (uint32_t index = 0, count = queuedRunnables.Length(); + index < count; + index++) { + nsCOMPtr<nsIRunnable> runnable; + queuedRunnables[index].swap(runnable); + + MOZ_ALWAYS_SUCCEEDS( + dbInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL)); + } + + queuedRunnables.Clear(); + } + + return true; +} + +void +ConnectionPool::NoteFinishedTransaction(uint64_t aTransactionId) +{ + AssertIsOnOwningThread(); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::NoteFinishedTransaction", + js::ProfileEntry::Category::STORAGE); + + TransactionInfo* transactionInfo = mTransactions.Get(aTransactionId); + MOZ_ASSERT(transactionInfo); + MOZ_ASSERT(transactionInfo->mRunning); + MOZ_ASSERT(transactionInfo->mFinished); + + transactionInfo->mRunning = false; + + DatabaseInfo* dbInfo = transactionInfo->mDatabaseInfo; + MOZ_ASSERT(dbInfo); + MOZ_ASSERT(mDatabases.Get(transactionInfo->mDatabaseId) == dbInfo); + MOZ_ASSERT(dbInfo->mThreadInfo.mThread); + MOZ_ASSERT(dbInfo->mThreadInfo.mRunnable); + + // Schedule the next write transaction if there are any queued. + if (dbInfo->mRunningWriteTransaction == transactionInfo) { + MOZ_ASSERT(transactionInfo->mIsWriteTransaction); + MOZ_ASSERT(dbInfo->mNeedsCheckpoint); + + dbInfo->mRunningWriteTransaction = nullptr; + + if (!dbInfo->mScheduledWriteTransactions.IsEmpty()) { + TransactionInfo* nextWriteTransaction = + dbInfo->mScheduledWriteTransactions[0]; + MOZ_ASSERT(nextWriteTransaction); + + dbInfo->mScheduledWriteTransactions.RemoveElementAt(0); + + MOZ_ALWAYS_TRUE(ScheduleTransaction(nextWriteTransaction, + /* aFromQueuedTransactions */ false)); + } + } + + const nsTArray<nsString>& objectStoreNames = + transactionInfo->mObjectStoreNames; + + for (uint32_t index = 0, count = objectStoreNames.Length(); + index < count; + index++) { + TransactionInfoPair* blockInfo = + dbInfo->mBlockingTransactions.Get(objectStoreNames[index]); + MOZ_ASSERT(blockInfo); + + if (transactionInfo->mIsWriteTransaction && + blockInfo->mLastBlockingReads == transactionInfo) { + blockInfo->mLastBlockingReads = nullptr; + } + + blockInfo->mLastBlockingWrites.RemoveElement(transactionInfo); + } + + transactionInfo->RemoveBlockingTransactions(); + + if (transactionInfo->mIsWriteTransaction) { + MOZ_ASSERT(dbInfo->mWriteTransactionCount); + dbInfo->mWriteTransactionCount--; + } else { + MOZ_ASSERT(dbInfo->mReadTransactionCount); + dbInfo->mReadTransactionCount--; + } + + mTransactions.Remove(aTransactionId); + +#ifdef DEBUG + // That just deleted |transactionInfo|. + transactionInfo = nullptr; +#endif + + if (!dbInfo->TotalTransactionCount()) { + MOZ_ASSERT(!dbInfo->mIdle); + dbInfo->mIdle = true; + + NoteIdleDatabase(dbInfo); + } +} + +void +ConnectionPool::ScheduleQueuedTransactions(ThreadInfo& aThreadInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aThreadInfo.mThread); + MOZ_ASSERT(aThreadInfo.mRunnable); + MOZ_ASSERT(!mQueuedTransactions.IsEmpty()); + MOZ_ASSERT(!mIdleThreads.Contains(aThreadInfo)); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::ScheduleQueuedTransactions", + js::ProfileEntry::Category::STORAGE); + + mIdleThreads.InsertElementSorted(aThreadInfo); + + aThreadInfo.mRunnable = nullptr; + aThreadInfo.mThread = nullptr; + + uint32_t index = 0; + for (uint32_t count = mQueuedTransactions.Length(); index < count; index++) { + if (!ScheduleTransaction(mQueuedTransactions[index], + /* aFromQueuedTransactions */ true)) { + break; + } + } + + if (index) { + mQueuedTransactions.RemoveElementsAt(0, index); + } + + AdjustIdleTimer(); +} + +void +ConnectionPool::NoteIdleDatabase(DatabaseInfo* aDatabaseInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabaseInfo); + MOZ_ASSERT(!aDatabaseInfo->TotalTransactionCount()); + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mThread); + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); + MOZ_ASSERT(!mIdleDatabases.Contains(aDatabaseInfo)); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::NoteIdleDatabase", + js::ProfileEntry::Category::STORAGE); + + const bool otherDatabasesWaiting = !mQueuedTransactions.IsEmpty(); + + if (mShutdownRequested || + otherDatabasesWaiting || + aDatabaseInfo->mCloseOnIdle) { + // Make sure we close the connection if we're shutting down or giving the + // thread to another database. + CloseDatabase(aDatabaseInfo); + + if (otherDatabasesWaiting) { + // Let another database use this thread. + ScheduleQueuedTransactions(aDatabaseInfo->mThreadInfo); + } else if (mShutdownRequested) { + // If there are no other databases that need to run then we can shut this + // thread down immediately instead of going through the idle thread + // mechanism. + ShutdownThread(aDatabaseInfo->mThreadInfo); + } + + return; + } + + mIdleDatabases.InsertElementSorted(aDatabaseInfo); + + AdjustIdleTimer(); +} + +void +ConnectionPool::NoteClosedDatabase(DatabaseInfo* aDatabaseInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabaseInfo); + MOZ_ASSERT(aDatabaseInfo->mClosing); + MOZ_ASSERT(!mIdleDatabases.Contains(aDatabaseInfo)); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::NoteClosedDatabase", + js::ProfileEntry::Category::STORAGE); + + aDatabaseInfo->mClosing = false; + + // Figure out what to do with this database's thread. It may have already been + // given to another database, in which case there's nothing to do here. + // Otherwise we prioritize the thread as follows: + // 1. Databases that haven't had an opportunity to run at all are highest + // priority. Those live in the |mQueuedTransactions| list. + // 2. If this database has additional transactions that were started after + // we began closing the connection then the thread can be reused for + // those transactions. + // 3. If we're shutting down then we can get rid of the thread. + // 4. Finally, if nothing above took the thread then we can add it to our + // list of idle threads. It may be reused or it may time out. If we have + // too many idle threads then we will shut down the oldest. + if (aDatabaseInfo->mThreadInfo.mThread) { + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); + + if (!mQueuedTransactions.IsEmpty()) { + // Give the thread to another database. + ScheduleQueuedTransactions(aDatabaseInfo->mThreadInfo); + } else if (!aDatabaseInfo->TotalTransactionCount()) { + if (mShutdownRequested) { + ShutdownThread(aDatabaseInfo->mThreadInfo); + } else { + MOZ_ASSERT(!mIdleThreads.Contains(aDatabaseInfo->mThreadInfo)); + + mIdleThreads.InsertElementSorted(aDatabaseInfo->mThreadInfo); + + aDatabaseInfo->mThreadInfo.mRunnable = nullptr; + aDatabaseInfo->mThreadInfo.mThread = nullptr; + + if (mIdleThreads.Length() > kMaxIdleConnectionThreadCount) { + ShutdownThread(mIdleThreads[0].mThreadInfo); + mIdleThreads.RemoveElementAt(0); + } + + AdjustIdleTimer(); + } + } + } + + // Schedule any transactions that were started while we were closing the + // connection. + if (aDatabaseInfo->TotalTransactionCount()) { + nsTArray<TransactionInfo*>& scheduledTransactions = + aDatabaseInfo->mTransactionsScheduledDuringClose; + + MOZ_ASSERT(!scheduledTransactions.IsEmpty()); + + for (uint32_t index = 0, count = scheduledTransactions.Length(); + index < count; + index++) { + Unused << ScheduleTransaction(scheduledTransactions[index], + /* aFromQueuedTransactions */ false); + } + + scheduledTransactions.Clear(); + + return; + } + + // There are no more transactions and the connection has been closed. We're + // done with this database. + { + MutexAutoLock lock(mDatabasesMutex); + + mDatabases.Remove(aDatabaseInfo->mDatabaseId); + } + +#ifdef DEBUG + // That just deleted |aDatabaseInfo|. + aDatabaseInfo = nullptr; +#endif + + // See if we need to fire any complete callbacks now that the database is + // finished. + for (uint32_t index = 0; + index < mCompleteCallbacks.Length(); + /* conditionally incremented */) { + if (MaybeFireCallback(mCompleteCallbacks[index])) { + mCompleteCallbacks.RemoveElementAt(index); + } else { + index++; + } + } + + // If that was the last database and we're supposed to be shutting down then + // we are finished. + if (mShutdownRequested && !mDatabases.Count()) { + MOZ_ASSERT(!mTransactions.Count()); + Cleanup(); + } +} + +bool +ConnectionPool::MaybeFireCallback(DatabasesCompleteCallback* aCallback) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!aCallback->mDatabaseIds.IsEmpty()); + MOZ_ASSERT(aCallback->mCallback); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::MaybeFireCallback", + js::ProfileEntry::Category::STORAGE); + + for (uint32_t count = aCallback->mDatabaseIds.Length(), index = 0; + index < count; + index++) { + const nsCString& databaseId = aCallback->mDatabaseIds[index]; + MOZ_ASSERT(!databaseId.IsEmpty()); + + if (mDatabases.Get(databaseId)) { + return false; + } + } + + Unused << aCallback->mCallback->Run(); + return true; +} + +void +ConnectionPool::PerformIdleDatabaseMaintenance(DatabaseInfo* aDatabaseInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabaseInfo); + MOZ_ASSERT(!aDatabaseInfo->TotalTransactionCount()); + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mThread); + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); + MOZ_ASSERT(aDatabaseInfo->mIdle); + MOZ_ASSERT(!aDatabaseInfo->mCloseOnIdle); + MOZ_ASSERT(!aDatabaseInfo->mClosing); + MOZ_ASSERT(mIdleDatabases.Contains(aDatabaseInfo)); + MOZ_ASSERT(!mDatabasesPerformingIdleMaintenance.Contains(aDatabaseInfo)); + + nsCOMPtr<nsIRunnable> runnable = + new IdleConnectionRunnable(aDatabaseInfo, aDatabaseInfo->mNeedsCheckpoint); + + aDatabaseInfo->mNeedsCheckpoint = false; + aDatabaseInfo->mIdle = false; + + mDatabasesPerformingIdleMaintenance.AppendElement(aDatabaseInfo); + + MOZ_ALWAYS_SUCCEEDS( + aDatabaseInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL)); +} + +void +ConnectionPool::CloseDatabase(DatabaseInfo* aDatabaseInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabaseInfo); + MOZ_ASSERT(!aDatabaseInfo->TotalTransactionCount()); + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mThread); + MOZ_ASSERT(aDatabaseInfo->mThreadInfo.mRunnable); + MOZ_ASSERT(!aDatabaseInfo->mClosing); + + aDatabaseInfo->mIdle = false; + aDatabaseInfo->mNeedsCheckpoint = false; + aDatabaseInfo->mClosing = true; + + nsCOMPtr<nsIRunnable> runnable = new CloseConnectionRunnable(aDatabaseInfo); + + MOZ_ALWAYS_SUCCEEDS( + aDatabaseInfo->mThreadInfo.mThread->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL)); +} + +bool +ConnectionPool::CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseId.IsEmpty()); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::CloseDatabaseWhenIdleInternal", + js::ProfileEntry::Category::STORAGE); + + if (DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId)) { + if (mIdleDatabases.RemoveElement(dbInfo) || + mDatabasesPerformingIdleMaintenance.RemoveElement(dbInfo)) { + CloseDatabase(dbInfo); + AdjustIdleTimer(); + } else { + dbInfo->mCloseOnIdle = true; + } + + return true; + } + + return false; +} + +ConnectionPool:: +ConnectionRunnable::ConnectionRunnable(DatabaseInfo* aDatabaseInfo) + : mDatabaseInfo(aDatabaseInfo) + , mOwningThread(do_GetCurrentThread()) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseInfo); + MOZ_ASSERT(aDatabaseInfo->mConnectionPool); + aDatabaseInfo->mConnectionPool->AssertIsOnOwningThread(); + MOZ_ASSERT(mOwningThread); +} + +NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::IdleConnectionRunnable, + ConnectionPool::ConnectionRunnable) + +NS_IMETHODIMP +ConnectionPool:: +IdleConnectionRunnable::Run() +{ + MOZ_ASSERT(mDatabaseInfo); + MOZ_ASSERT(!mDatabaseInfo->mIdle); + + nsCOMPtr<nsIEventTarget> owningThread; + mOwningThread.swap(owningThread); + + if (owningThread) { + mDatabaseInfo->AssertIsOnConnectionThread(); + + // The connection could be null if EnsureConnection() didn't run or was not + // successful in TransactionDatabaseOperationBase::RunOnConnectionThread(). + if (mDatabaseInfo->mConnection) { + mDatabaseInfo->mConnection->DoIdleProcessing(mNeedsCheckpoint); + + MOZ_ALWAYS_SUCCEEDS( + owningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + return NS_OK; + } + } + + RefPtr<ConnectionPool> connectionPool = mDatabaseInfo->mConnectionPool; + MOZ_ASSERT(connectionPool); + + if (mDatabaseInfo->mClosing || mDatabaseInfo->TotalTransactionCount()) { + MOZ_ASSERT(!connectionPool-> + mDatabasesPerformingIdleMaintenance.Contains(mDatabaseInfo)); + } else { + MOZ_ALWAYS_TRUE( + connectionPool-> + mDatabasesPerformingIdleMaintenance.RemoveElement(mDatabaseInfo)); + + connectionPool->NoteIdleDatabase(mDatabaseInfo); + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::CloseConnectionRunnable, + ConnectionPool::ConnectionRunnable) + +NS_IMETHODIMP +ConnectionPool:: +CloseConnectionRunnable::Run() +{ + MOZ_ASSERT(mDatabaseInfo); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::CloseConnectionRunnable::Run", + js::ProfileEntry::Category::STORAGE); + + if (mOwningThread) { + MOZ_ASSERT(mDatabaseInfo->mClosing); + + nsCOMPtr<nsIEventTarget> owningThread; + mOwningThread.swap(owningThread); + + // The connection could be null if EnsureConnection() didn't run or was not + // successful in TransactionDatabaseOperationBase::RunOnConnectionThread(). + if (mDatabaseInfo->mConnection) { + mDatabaseInfo->AssertIsOnConnectionThread(); + + mDatabaseInfo->mConnection->Close(); + + IDB_DEBUG_LOG(("ConnectionPool closed connection 0x%p", + mDatabaseInfo->mConnection.get())); + + mDatabaseInfo->mConnection = nullptr; + +#ifdef DEBUG + mDatabaseInfo->mDEBUGConnectionThread = nullptr; +#endif + } + + MOZ_ALWAYS_SUCCEEDS( + owningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + return NS_OK; + } + + RefPtr<ConnectionPool> connectionPool = mDatabaseInfo->mConnectionPool; + MOZ_ASSERT(connectionPool); + + connectionPool->NoteClosedDatabase(mDatabaseInfo); + return NS_OK; +} + +ConnectionPool:: +DatabaseInfo::DatabaseInfo(ConnectionPool* aConnectionPool, + const nsACString& aDatabaseId) + : mConnectionPool(aConnectionPool) + , mDatabaseId(aDatabaseId) + , mRunningWriteTransaction(nullptr) + , mReadTransactionCount(0) + , mWriteTransactionCount(0) + , mNeedsCheckpoint(false) + , mIdle(false) + , mCloseOnIdle(false) + , mClosing(false) +#ifdef DEBUG + , mDEBUGConnectionThread(nullptr) +#endif +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aConnectionPool); + aConnectionPool->AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseId.IsEmpty()); + + MOZ_COUNT_CTOR(ConnectionPool::DatabaseInfo); +} + +ConnectionPool:: +DatabaseInfo::~DatabaseInfo() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(mScheduledWriteTransactions.IsEmpty()); + MOZ_ASSERT(!mRunningWriteTransaction); + MOZ_ASSERT(!mThreadInfo.mThread); + MOZ_ASSERT(!mThreadInfo.mRunnable); + MOZ_ASSERT(!TotalTransactionCount()); + + MOZ_COUNT_DTOR(ConnectionPool::DatabaseInfo); +} + +ConnectionPool:: +DatabasesCompleteCallback::DatabasesCompleteCallback( + nsTArray<nsCString>&& aDatabaseIds, + nsIRunnable* aCallback) + : mDatabaseIds(Move(aDatabaseIds)) + , mCallback(aCallback) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mDatabaseIds.IsEmpty()); + MOZ_ASSERT(aCallback); + + MOZ_COUNT_CTOR(ConnectionPool::DatabasesCompleteCallback); +} + +ConnectionPool:: +DatabasesCompleteCallback::~DatabasesCompleteCallback() +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::DatabasesCompleteCallback); +} + +ConnectionPool:: +FinishCallbackWrapper::FinishCallbackWrapper(ConnectionPool* aConnectionPool, + uint64_t aTransactionId, + FinishCallback* aCallback) + : mConnectionPool(aConnectionPool) + , mCallback(aCallback) + , mOwningThread(do_GetCurrentThread()) + , mTransactionId(aTransactionId) + , mHasRunOnce(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aConnectionPool); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(mOwningThread); +} + +ConnectionPool:: +FinishCallbackWrapper::~FinishCallbackWrapper() +{ + MOZ_ASSERT(!mConnectionPool); + MOZ_ASSERT(!mCallback); +} + +NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::FinishCallbackWrapper, Runnable) + +nsresult +ConnectionPool:: +FinishCallbackWrapper::Run() +{ + MOZ_ASSERT(mConnectionPool); + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mOwningThread); + + PROFILER_LABEL("IndexedDB", + "ConnectionPool::FinishCallbackWrapper::Run", + js::ProfileEntry::Category::STORAGE); + + if (!mHasRunOnce) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + mHasRunOnce = true; + + Unused << mCallback->Run(); + + MOZ_ALWAYS_SUCCEEDS( + mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; + } + + mConnectionPool->AssertIsOnOwningThread(); + MOZ_ASSERT(mHasRunOnce); + + RefPtr<ConnectionPool> connectionPool = Move(mConnectionPool); + RefPtr<FinishCallback> callback = Move(mCallback); + + callback->TransactionFinishedBeforeUnblock(); + + connectionPool->NoteFinishedTransaction(mTransactionId); + + callback->TransactionFinishedAfterUnblock(); + + return NS_OK; +} + +uint32_t ConnectionPool::ThreadRunnable::sNextSerialNumber = 0; + +ConnectionPool:: +ThreadRunnable::ThreadRunnable() + : mSerialNumber(++sNextSerialNumber) + , mFirstRun(true) + , mContinueRunning(true) +{ + AssertIsOnBackgroundThread(); +} + +ConnectionPool:: +ThreadRunnable::~ThreadRunnable() +{ + MOZ_ASSERT(!mFirstRun); + MOZ_ASSERT(!mContinueRunning); +} + +NS_IMPL_ISUPPORTS_INHERITED0(ConnectionPool::ThreadRunnable, Runnable) + +nsresult +ConnectionPool:: +ThreadRunnable::Run() +{ +#ifdef MOZ_ENABLE_PROFILER_SPS + char stackTopGuess; +#endif // MOZ_ENABLE_PROFILER_SPS + + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mContinueRunning); + + if (!mFirstRun) { + mContinueRunning = false; + return NS_OK; + } + + mFirstRun = false; + + { + // Scope for the thread name. Both PR_SetCurrentThreadName() and + // profiler_register_thread() copy the string so we don't need to keep it. + const nsPrintfCString threadName("IndexedDB #%lu", mSerialNumber); + + PR_SetCurrentThreadName(threadName.get()); + +#ifdef MOZ_ENABLE_PROFILER_SPS + profiler_register_thread(threadName.get(), &stackTopGuess); +#endif // MOZ_ENABLE_PROFILER_SPS + } + + { + // Scope for the profiler label. + PROFILER_LABEL("IndexedDB", + "ConnectionPool::ThreadRunnable::Run", + js::ProfileEntry::Category::STORAGE); + + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + +#ifdef DEBUG + if (kDEBUGTransactionThreadPriority != + nsISupportsPriority::PRIORITY_NORMAL) { + NS_WARNING("ConnectionPool thread debugging enabled, priority has been " + "modified!"); + + nsCOMPtr<nsISupportsPriority> thread = do_QueryInterface(currentThread); + MOZ_ASSERT(thread); + + MOZ_ALWAYS_SUCCEEDS( + thread->SetPriority(kDEBUGTransactionThreadPriority)); + } + + if (kDEBUGTransactionThreadSleepMS) { + NS_WARNING("TransactionThreadPool thread debugging enabled, sleeping " + "after every event!"); + } +#endif // DEBUG + + while (mContinueRunning) { + MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread)); + +#ifdef DEBUG + if (kDEBUGTransactionThreadSleepMS) { + MOZ_ALWAYS_TRUE( + PR_Sleep(PR_MillisecondsToInterval(kDEBUGTransactionThreadSleepMS)) == + PR_SUCCESS); + } +#endif // DEBUG + } + } + +#ifdef MOZ_ENABLE_PROFILER_SPS + profiler_unregister_thread(); +#endif // MOZ_ENABLE_PROFILER_SPS + + return NS_OK; +} + +ConnectionPool:: +ThreadInfo::ThreadInfo() +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); +} + +ConnectionPool:: +ThreadInfo::ThreadInfo(const ThreadInfo& aOther) + : mThread(aOther.mThread) + , mRunnable(aOther.mRunnable) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aOther.mThread); + MOZ_ASSERT(aOther.mRunnable); + + MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); +} + +ConnectionPool:: +ThreadInfo::~ThreadInfo() +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::ThreadInfo); +} + +ConnectionPool:: +IdleResource::IdleResource(const TimeStamp& aIdleTime) + : mIdleTime(aIdleTime) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!aIdleTime.IsNull()); + + MOZ_COUNT_CTOR(ConnectionPool::IdleResource); +} + +ConnectionPool:: +IdleResource::~IdleResource() +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::IdleResource); +} + +ConnectionPool:: +IdleDatabaseInfo::IdleDatabaseInfo(DatabaseInfo* aDatabaseInfo) + : IdleResource(TimeStamp::NowLoRes() + + (aDatabaseInfo->mIdle ? + TimeDuration::FromMilliseconds(kConnectionIdleMaintenanceMS) : + TimeDuration::FromMilliseconds(kConnectionIdleCloseMS))) + , mDatabaseInfo(aDatabaseInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseInfo); + + MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo); +} + +ConnectionPool:: +IdleDatabaseInfo::~IdleDatabaseInfo() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabaseInfo); + + MOZ_COUNT_DTOR(ConnectionPool::IdleDatabaseInfo); +} + +ConnectionPool:: +IdleThreadInfo::IdleThreadInfo(const ThreadInfo& aThreadInfo) + : IdleResource(TimeStamp::NowLoRes() + + TimeDuration::FromMilliseconds(kConnectionThreadIdleMS)) + , mThreadInfo(aThreadInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aThreadInfo.mRunnable); + MOZ_ASSERT(aThreadInfo.mThread); + + MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo); +} + +ConnectionPool:: +IdleThreadInfo::~IdleThreadInfo() +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::IdleThreadInfo); +} + +ConnectionPool:: +TransactionInfo::TransactionInfo( + DatabaseInfo* aDatabaseInfo, + const nsID& aBackgroundChildLoggingId, + const nsACString& aDatabaseId, + uint64_t aTransactionId, + int64_t aLoggingSerialNumber, + const nsTArray<nsString>& aObjectStoreNames, + bool aIsWriteTransaction, + TransactionDatabaseOperationBase* aTransactionOp) + : mDatabaseInfo(aDatabaseInfo) + , mBackgroundChildLoggingId(aBackgroundChildLoggingId) + , mDatabaseId(aDatabaseId) + , mTransactionId(aTransactionId) + , mLoggingSerialNumber(aLoggingSerialNumber) + , mObjectStoreNames(aObjectStoreNames) + , mIsWriteTransaction(aIsWriteTransaction) + , mRunning(false) +#ifdef DEBUG + , mFinished(false) +#endif +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseInfo); + aDatabaseInfo->mConnectionPool->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(ConnectionPool::TransactionInfo); + + if (aTransactionOp) { + mQueuedRunnables.AppendElement(aTransactionOp); + } +} + +ConnectionPool:: +TransactionInfo::~TransactionInfo() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mBlockedOn.Count()); + MOZ_ASSERT(mQueuedRunnables.IsEmpty()); + MOZ_ASSERT(!mRunning); + MOZ_ASSERT(mFinished); + + MOZ_COUNT_DTOR(ConnectionPool::TransactionInfo); +} + +void +ConnectionPool:: +TransactionInfo::AddBlockingTransaction(TransactionInfo* aTransactionInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransactionInfo); + + if (!mBlocking.Contains(aTransactionInfo)) { + mBlocking.PutEntry(aTransactionInfo); + mBlockingOrdered.AppendElement(aTransactionInfo); + } +} + +void +ConnectionPool:: +TransactionInfo::RemoveBlockingTransactions() +{ + AssertIsOnBackgroundThread(); + + for (uint32_t index = 0, count = mBlockingOrdered.Length(); + index < count; + index++) { + TransactionInfo* blockedInfo = mBlockingOrdered[index]; + MOZ_ASSERT(blockedInfo); + + blockedInfo->MaybeUnblock(this); + } + + mBlocking.Clear(); + mBlockingOrdered.Clear(); +} + +void +ConnectionPool:: +TransactionInfo::MaybeUnblock(TransactionInfo* aTransactionInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mBlockedOn.Contains(aTransactionInfo)); + + mBlockedOn.RemoveEntry(aTransactionInfo); + if (!mBlockedOn.Count()) { + MOZ_ASSERT(mDatabaseInfo); + + ConnectionPool* connectionPool = mDatabaseInfo->mConnectionPool; + MOZ_ASSERT(connectionPool); + connectionPool->AssertIsOnOwningThread(); + + Unused << + connectionPool->ScheduleTransaction(this, + /* aFromQueuedTransactions */ false); + } +} + +ConnectionPool:: +TransactionInfoPair::TransactionInfoPair() + : mLastBlockingReads(nullptr) +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_CTOR(ConnectionPool::TransactionInfoPair); +} + +ConnectionPool:: +TransactionInfoPair::~TransactionInfoPair() +{ + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::TransactionInfoPair); +} + +/******************************************************************************* + * Metadata classes + ******************************************************************************/ + +bool +FullObjectStoreMetadata::HasLiveIndexes() const +{ + AssertIsOnBackgroundThread(); + + for (auto iter = mIndexes.ConstIter(); !iter.Done(); iter.Next()) { + if (!iter.Data()->mDeleted) { + return true; + } + } + + return false; +} + +already_AddRefed<FullDatabaseMetadata> +FullDatabaseMetadata::Duplicate() const +{ + AssertIsOnBackgroundThread(); + + // FullDatabaseMetadata contains two hash tables of pointers that we need to + // duplicate so we can't just use the copy constructor. + RefPtr<FullDatabaseMetadata> newMetadata = + new FullDatabaseMetadata(mCommonMetadata); + + newMetadata->mDatabaseId = mDatabaseId; + newMetadata->mFilePath = mFilePath; + newMetadata->mNextObjectStoreId = mNextObjectStoreId; + newMetadata->mNextIndexId = mNextIndexId; + + for (auto iter = mObjectStores.ConstIter(); !iter.Done(); iter.Next()) { + auto key = iter.Key(); + auto value = iter.Data(); + + RefPtr<FullObjectStoreMetadata> newOSMetadata = + new FullObjectStoreMetadata(); + + newOSMetadata->mCommonMetadata = value->mCommonMetadata; + newOSMetadata->mNextAutoIncrementId = value->mNextAutoIncrementId; + newOSMetadata->mCommittedAutoIncrementId = value->mCommittedAutoIncrementId; + + for (auto iter = value->mIndexes.ConstIter(); !iter.Done(); iter.Next()) { + auto key = iter.Key(); + auto value = iter.Data(); + + RefPtr<FullIndexMetadata> newIndexMetadata = new FullIndexMetadata(); + + newIndexMetadata->mCommonMetadata = value->mCommonMetadata; + + if (NS_WARN_IF(!newOSMetadata->mIndexes.Put(key, newIndexMetadata, + fallible))) { + return nullptr; + } + } + + MOZ_ASSERT(value->mIndexes.Count() == newOSMetadata->mIndexes.Count()); + + if (NS_WARN_IF(!newMetadata->mObjectStores.Put(key, newOSMetadata, + fallible))) { + return nullptr; + } + } + + MOZ_ASSERT(mObjectStores.Count() == newMetadata->mObjectStores.Count()); + + return newMetadata.forget(); +} + +DatabaseLoggingInfo::~DatabaseLoggingInfo() +{ + AssertIsOnBackgroundThread(); + + if (gLoggingInfoHashtable) { + const nsID& backgroundChildLoggingId = + mLoggingInfo.backgroundChildLoggingId(); + + MOZ_ASSERT(gLoggingInfoHashtable->Get(backgroundChildLoggingId) == this); + + gLoggingInfoHashtable->Remove(backgroundChildLoggingId); + } +} + +/******************************************************************************* + * Factory + ******************************************************************************/ + +Factory::Factory(already_AddRefed<DatabaseLoggingInfo> aLoggingInfo) + : mLoggingInfo(Move(aLoggingInfo)) +#ifdef DEBUG + , mActorDestroyed(false) +#endif +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); +} + +Factory::~Factory() +{ + MOZ_ASSERT(mActorDestroyed); +} + +// static +already_AddRefed<Factory> +Factory::Create(const LoggingInfo& aLoggingInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // Balanced in ActoryDestroy(). + IncreaseBusyCount(); + + MOZ_ASSERT(gLoggingInfoHashtable); + RefPtr<DatabaseLoggingInfo> loggingInfo = + gLoggingInfoHashtable->Get(aLoggingInfo.backgroundChildLoggingId()); + if (loggingInfo) { + MOZ_ASSERT(aLoggingInfo.backgroundChildLoggingId() == loggingInfo->Id()); +#if !DISABLE_ASSERTS_FOR_FUZZING + NS_WARNING_ASSERTION( + aLoggingInfo.nextTransactionSerialNumber() == + loggingInfo->mLoggingInfo.nextTransactionSerialNumber(), + "NextTransactionSerialNumber doesn't match!"); + NS_WARNING_ASSERTION( + aLoggingInfo.nextVersionChangeTransactionSerialNumber() == + loggingInfo->mLoggingInfo. + nextVersionChangeTransactionSerialNumber(), + "NextVersionChangeTransactionSerialNumber doesn't match!"); + NS_WARNING_ASSERTION( + aLoggingInfo.nextRequestSerialNumber() == + loggingInfo->mLoggingInfo.nextRequestSerialNumber(), + "NextRequestSerialNumber doesn't match!"); +#endif // !DISABLE_ASSERTS_FOR_FUZZING + } else { + loggingInfo = new DatabaseLoggingInfo(aLoggingInfo); + gLoggingInfoHashtable->Put(aLoggingInfo.backgroundChildLoggingId(), + loggingInfo); + } + + RefPtr<Factory> actor = new Factory(loggingInfo.forget()); + + return actor.forget(); +} + +void +Factory::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + +#ifdef DEBUG + mActorDestroyed = true; +#endif + + // Match the IncreaseBusyCount in Create(). + DecreaseBusyCount(); +} + +bool +Factory::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + return PBackgroundIDBFactoryParent::Send__delete__(this); +} + +bool +Factory::RecvIncrementLoggingRequestSerialNumber() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mLoggingInfo); + + mLoggingInfo->NextRequestSN(); + return true; +} + +PBackgroundIDBFactoryRequestParent* +Factory::AllocPBackgroundIDBFactoryRequestParent( + const FactoryRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != FactoryRequestParams::T__None); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + const CommonFactoryRequestParams* commonParams; + + switch (aParams.type()) { + case FactoryRequestParams::TOpenDatabaseRequestParams: { + const OpenDatabaseRequestParams& params = + aParams.get_OpenDatabaseRequestParams(); + commonParams = ¶ms.commonParams(); + break; + } + + case FactoryRequestParams::TDeleteDatabaseRequestParams: { + const DeleteDatabaseRequestParams& params = + aParams.get_DeleteDatabaseRequestParams(); + commonParams = ¶ms.commonParams(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + MOZ_ASSERT(commonParams); + + const DatabaseMetadata& metadata = commonParams->metadata(); + if (NS_WARN_IF(metadata.persistenceType() != PERSISTENCE_TYPE_PERSISTENT && + metadata.persistenceType() != PERSISTENCE_TYPE_TEMPORARY && + metadata.persistenceType() != PERSISTENCE_TYPE_DEFAULT)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + const PrincipalInfo& principalInfo = commonParams->principalInfo(); + if (NS_WARN_IF(principalInfo.type() == PrincipalInfo::TNullPrincipalInfo)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && + metadata.persistenceType() != PERSISTENCE_TYPE_PERSISTENT)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr<ContentParent> contentParent = + BackgroundParent::GetContentParent(Manager()); + + RefPtr<FactoryOp> actor; + if (aParams.type() == FactoryRequestParams::TOpenDatabaseRequestParams) { + actor = new OpenDatabaseOp(this, + contentParent.forget(), + *commonParams); + } else { + actor = new DeleteDatabaseOp(this, contentParent.forget(), *commonParams); + } + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool +Factory::RecvPBackgroundIDBFactoryRequestConstructor( + PBackgroundIDBFactoryRequestParent* aActor, + const FactoryRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != FactoryRequestParams::T__None); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + auto* op = static_cast<FactoryOp*>(aActor); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(op)); + return true; +} + +bool +Factory::DeallocPBackgroundIDBFactoryRequestParent( + PBackgroundIDBFactoryRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<FactoryOp> op = dont_AddRef(static_cast<FactoryOp*>(aActor)); + return true; +} + +PBackgroundIDBDatabaseParent* +Factory::AllocPBackgroundIDBDatabaseParent( + const DatabaseSpec& aSpec, + PBackgroundIDBFactoryRequestParent* aRequest) +{ + MOZ_CRASH("PBackgroundIDBDatabaseParent actors should be constructed " + "manually!"); +} + +bool +Factory::DeallocPBackgroundIDBDatabaseParent( + PBackgroundIDBDatabaseParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<Database> database = dont_AddRef(static_cast<Database*>(aActor)); + return true; +} + +/******************************************************************************* + * WaitForTransactionsHelper + ******************************************************************************/ + +void +WaitForTransactionsHelper::WaitForTransactions() +{ + MOZ_ASSERT(mState == State::Initial); + + Unused << this->Run(); +} + +void +WaitForTransactionsHelper::MaybeWaitForTransactions() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial); + + RefPtr<ConnectionPool> connectionPool = gConnectionPool.get(); + if (connectionPool) { + nsTArray<nsCString> ids(1); + ids.AppendElement(mDatabaseId); + + mState = State::WaitingForTransactions; + + connectionPool->WaitForDatabasesToComplete(Move(ids), this); + return; + } + + MaybeWaitForFileHandles(); +} + +void +WaitForTransactionsHelper::MaybeWaitForFileHandles() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial || mState == State::WaitingForTransactions); + + RefPtr<FileHandleThreadPool> fileHandleThreadPool = + gFileHandleThreadPool.get(); + if (fileHandleThreadPool) { + nsTArray<nsCString> ids(1); + ids.AppendElement(mDatabaseId); + + mState = State::WaitingForFileHandles; + + fileHandleThreadPool->WaitForDirectoriesToComplete(Move(ids), this); + return; + } + + CallCallback(); +} + +void +WaitForTransactionsHelper::CallCallback() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial || + mState == State::WaitingForTransactions || + mState == State::WaitingForFileHandles); + + nsCOMPtr<nsIRunnable> callback; + mCallback.swap(callback); + + callback->Run(); + + mState = State::Complete; +} + +NS_IMPL_ISUPPORTS_INHERITED0(WaitForTransactionsHelper, Runnable) + +NS_IMETHODIMP +WaitForTransactionsHelper::Run() +{ + MOZ_ASSERT(mState != State::Complete); + MOZ_ASSERT(mCallback); + + switch (mState) { + case State::Initial: + MaybeWaitForTransactions(); + break; + + case State::WaitingForTransactions: + MaybeWaitForFileHandles(); + break; + + case State::WaitingForFileHandles: + CallCallback(); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + return NS_OK; +} + +/******************************************************************************* + * Database + ******************************************************************************/ + +Database::Database(Factory* aFactory, + const PrincipalInfo& aPrincipalInfo, + const Maybe<ContentParentId>& aOptionalContentParentId, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId, + FullDatabaseMetadata* aMetadata, + FileManager* aFileManager, + already_AddRefed<DirectoryLock> aDirectoryLock, + bool aFileHandleDisabled, + bool aChromeWriteAccessAllowed) + : mFactory(aFactory) + , mMetadata(aMetadata) + , mFileManager(aFileManager) + , mDirectoryLock(Move(aDirectoryLock)) + , mPrincipalInfo(aPrincipalInfo) + , mOptionalContentParentId(aOptionalContentParentId) + , mGroup(aGroup) + , mOrigin(aOrigin) + , mId(aMetadata->mDatabaseId) + , mFilePath(aMetadata->mFilePath) + , mActiveMutableFileCount(0) + , mTelemetryId(aTelemetryId) + , mPersistenceType(aMetadata->mCommonMetadata.persistenceType()) + , mFileHandleDisabled(aFileHandleDisabled) + , mChromeWriteAccessAllowed(aChromeWriteAccessAllowed) + , mClosed(false) + , mInvalidated(false) + , mActorWasAlive(false) + , mActorDestroyed(false) + , mMetadataCleanedUp(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFactory); + MOZ_ASSERT(aMetadata); + MOZ_ASSERT(aFileManager); + MOZ_ASSERT_IF(aChromeWriteAccessAllowed, + aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo); +} + +void +Database::Invalidate() +{ + AssertIsOnBackgroundThread(); + + class MOZ_STACK_CLASS Helper final + { + public: + static bool + InvalidateTransactions(nsTHashtable<nsPtrHashKey<TransactionBase>>& aTable) + { + AssertIsOnBackgroundThread(); + + const uint32_t count = aTable.Count(); + if (!count) { + return true; + } + + FallibleTArray<RefPtr<TransactionBase>> transactions; + if (NS_WARN_IF(!transactions.SetCapacity(count, fallible))) { + return false; + } + + for (auto iter = aTable.Iter(); !iter.Done(); iter.Next()) { + if (NS_WARN_IF(!transactions.AppendElement(iter.Get()->GetKey(), + fallible))) { + return false; + } + } + + if (count) { + IDB_REPORT_INTERNAL_ERR(); + + for (uint32_t index = 0; index < count; index++) { + RefPtr<TransactionBase> transaction = transactions[index].forget(); + MOZ_ASSERT(transaction); + + transaction->Invalidate(); + } + } + + return true; + } + + static bool + InvalidateMutableFiles(nsTHashtable<nsPtrHashKey<MutableFile>>& aTable) + { + AssertIsOnBackgroundThread(); + + const uint32_t count = aTable.Count(); + if (!count) { + return true; + } + + FallibleTArray<RefPtr<MutableFile>> mutableFiles; + if (NS_WARN_IF(!mutableFiles.SetCapacity(count, fallible))) { + return false; + } + + for (auto iter = aTable.Iter(); !iter.Done(); iter.Next()) { + if (NS_WARN_IF(!mutableFiles.AppendElement(iter.Get()->GetKey(), + fallible))) { + return false; + } + } + + if (count) { + IDB_REPORT_INTERNAL_ERR(); + + for (uint32_t index = 0; index < count; index++) { + RefPtr<MutableFile> mutableFile = mutableFiles[index].forget(); + MOZ_ASSERT(mutableFile); + + mutableFile->Invalidate(); + } + } + + return true; + } + }; + + if (mInvalidated) { + return; + } + + mInvalidated = true; + + if (mActorWasAlive && !mActorDestroyed) { + Unused << SendInvalidate(); + } + + if (!Helper::InvalidateTransactions(mTransactions)) { + NS_WARNING("Failed to abort all transactions!"); + } + + if (!Helper::InvalidateMutableFiles(mMutableFiles)) { + NS_WARNING("Failed to abort all mutable files!"); + } + + MOZ_ALWAYS_TRUE(CloseInternal()); + + CleanupMetadata(); +} + +nsresult +Database::EnsureConnection() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + PROFILER_LABEL("IndexedDB", + "Database::EnsureConnection", + js::ProfileEntry::Category::STORAGE); + + if (!mConnection || !mConnection->GetStorageConnection()) { + nsresult rv = + gConnectionPool->GetOrCreateConnection(this, getter_AddRefs(mConnection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + AssertIsOnConnectionThread(); + + return NS_OK; +} + +bool +Database::RegisterTransaction(TransactionBase* aTransaction) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(!mTransactions.GetEntry(aTransaction)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mInvalidated); + MOZ_ASSERT(!mClosed); + + if (NS_WARN_IF(!mTransactions.PutEntry(aTransaction, fallible))) { + return false; + } + + return true; +} + +void +Database::UnregisterTransaction(TransactionBase* aTransaction) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(mTransactions.GetEntry(aTransaction)); + + mTransactions.RemoveEntry(aTransaction); + + MaybeCloseConnection(); +} + +bool +Database::RegisterMutableFile(MutableFile* aMutableFile) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aMutableFile); + MOZ_ASSERT(!mMutableFiles.GetEntry(aMutableFile)); + MOZ_ASSERT(mDirectoryLock); + + if (NS_WARN_IF(!mMutableFiles.PutEntry(aMutableFile, fallible))) { + return false; + } + + return true; +} + +void +Database::UnregisterMutableFile(MutableFile* aMutableFile) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aMutableFile); + MOZ_ASSERT(mMutableFiles.GetEntry(aMutableFile)); + + mMutableFiles.RemoveEntry(aMutableFile); +} + +void +Database::NoteActiveMutableFile() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(mActiveMutableFileCount < UINT32_MAX); + + ++mActiveMutableFileCount; +} + +void +Database::NoteInactiveMutableFile() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActiveMutableFileCount > 0); + + --mActiveMutableFileCount; + + MaybeCloseConnection(); +} + +void +Database::SetActorAlive() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorWasAlive); + MOZ_ASSERT(!mActorDestroyed); + + mActorWasAlive = true; + + // This reference will be absorbed by IPDL and released when the actor is + // destroyed. + AddRef(); +} + +bool +Database::CloseInternal() +{ + AssertIsOnBackgroundThread(); + + if (mClosed) { + if (NS_WARN_IF(!IsInvalidated())) { + // Kill misbehaving child for sending the close message twice. + return false; + } + + // Ignore harmless race when we just invalidated the database. + return true; + } + + mClosed = true; + + if (gConnectionPool) { + gConnectionPool->CloseDatabaseWhenIdle(Id()); + } + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); + + MOZ_ASSERT(info->mLiveDatabases.Contains(this)); + + if (info->mWaitingFactoryOp) { + info->mWaitingFactoryOp->NoteDatabaseClosed(this); + } + + MaybeCloseConnection(); + + return true; +} + +void +Database::MaybeCloseConnection() +{ + AssertIsOnBackgroundThread(); + + if (!mTransactions.Count() && + !mActiveMutableFileCount && + IsClosed() && + mDirectoryLock) { + nsCOMPtr<nsIRunnable> callback = + NewRunnableMethod(this, &Database::ConnectionClosedCallback); + + RefPtr<WaitForTransactionsHelper> helper = + new WaitForTransactionsHelper(Id(), callback); + helper->WaitForTransactions(); + } +} + +void +Database::ConnectionClosedCallback() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mClosed); + MOZ_ASSERT(!mTransactions.Count()); + MOZ_ASSERT(!mActiveMutableFileCount); + + mDirectoryLock = nullptr; + + CleanupMetadata(); + + if (IsInvalidated() && IsActorAlive()) { + // Step 3 and 4 of "5.2 Closing a Database": + // 1. Wait for all transactions to complete. + // 2. Fire a close event if forced flag is set, i.e., IsInvalidated() in our + // implementation. + Unused << SendCloseAfterInvalidationComplete(); + } +} + +void +Database::CleanupMetadata() +{ + AssertIsOnBackgroundThread(); + + if (!mMetadataCleanedUp) { + mMetadataCleanedUp = true; + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); + MOZ_ALWAYS_TRUE(info->mLiveDatabases.RemoveElement(this)); + + if (info->mLiveDatabases.IsEmpty()) { + MOZ_ASSERT(!info->mWaitingFactoryOp || + !info->mWaitingFactoryOp->HasBlockedDatabases()); + gLiveDatabaseHashtable->Remove(Id()); + } + + // Match the IncreaseBusyCount in OpenDatabaseOp::EnsureDatabaseActor(). + DecreaseBusyCount(); + } +} + +bool +Database::VerifyRequestParams(const DatabaseRequestParams& aParams) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != DatabaseRequestParams::T__None); + + switch (aParams.type()) { + case DatabaseRequestParams::TCreateFileParams: { + if (NS_WARN_IF(mFileHandleDisabled)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const CreateFileParams& params = aParams.get_CreateFileParams(); + + if (NS_WARN_IF(params.name().IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +void +Database::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + if (!IsInvalidated()) { + Invalidate(); + } +} + +PBackgroundIDBDatabaseFileParent* +Database::AllocPBackgroundIDBDatabaseFileParent(PBlobParent* aBlobParent) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aBlobParent); + + RefPtr<BlobImpl> blobImpl = + static_cast<BlobParent*>(aBlobParent)->GetBlobImpl(); + MOZ_ASSERT(blobImpl); + + RefPtr<FileInfo> fileInfo; + RefPtr<DatabaseFile> actor; + + RefPtr<BlobImplStoredFile> storedFileImpl = do_QueryObject(blobImpl); + if (storedFileImpl && storedFileImpl->IsShareable(mFileManager)) { + // This blob was previously shared with the child. + fileInfo = storedFileImpl->GetFileInfo(); + MOZ_ASSERT(fileInfo); + + actor = new DatabaseFile(fileInfo); + } else { + // This is a blob we haven't seen before. + fileInfo = mFileManager->GetNewFileInfo(); + MOZ_ASSERT(fileInfo); + + actor = new DatabaseFile(blobImpl, fileInfo); + } + + MOZ_ASSERT(actor); + + return actor.forget().take(); +} + +bool +Database::DeallocPBackgroundIDBDatabaseFileParent( + PBackgroundIDBDatabaseFileParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<DatabaseFile> actor = + dont_AddRef(static_cast<DatabaseFile*>(aActor)); + return true; +} + +PBackgroundIDBDatabaseRequestParent* +Database::AllocPBackgroundIDBDatabaseRequestParent( + const DatabaseRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != DatabaseRequestParams::T__None); + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + PBackgroundParent* backgroundActor = GetBackgroundParent(); + MOZ_ASSERT(backgroundActor); + + bool trustParams = !BackgroundParent::IsOtherProcessActor(backgroundActor); +#endif + + if (NS_WARN_IF(!trustParams && !VerifyRequestParams(aParams))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr<DatabaseOp> actor; + + switch (aParams.type()) { + case DatabaseRequestParams::TCreateFileParams: { + actor = new CreateFileOp(this, aParams); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + MOZ_ASSERT(actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool +Database::RecvPBackgroundIDBDatabaseRequestConstructor( + PBackgroundIDBDatabaseRequestParent* aActor, + const DatabaseRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != DatabaseRequestParams::T__None); + + auto* op = static_cast<DatabaseOp*>(aActor); + + op->RunImmediately(); + + return true; +} + +bool +Database::DeallocPBackgroundIDBDatabaseRequestParent( + PBackgroundIDBDatabaseRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<DatabaseOp> op = dont_AddRef(static_cast<DatabaseOp*>(aActor)); + return true; +} + +PBackgroundIDBTransactionParent* +Database::AllocPBackgroundIDBTransactionParent( + const nsTArray<nsString>& aObjectStoreNames, + const Mode& aMode) +{ + AssertIsOnBackgroundThread(); + + // Once a database is closed it must not try to open new transactions. + if (NS_WARN_IF(mClosed)) { + if (!mInvalidated) { + ASSERT_UNLESS_FUZZING(); + } + return nullptr; + } + + if (NS_WARN_IF(aObjectStoreNames.IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(aMode != IDBTransaction::READ_ONLY && + aMode != IDBTransaction::READ_WRITE && + aMode != IDBTransaction::READ_WRITE_FLUSH && + aMode != IDBTransaction::CLEANUP)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + // If this is a readwrite transaction to a chrome database make sure the child + // has write access. + if (NS_WARN_IF((aMode == IDBTransaction::READ_WRITE || + aMode == IDBTransaction::READ_WRITE_FLUSH || + aMode == IDBTransaction::CLEANUP) && + mPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && + !mChromeWriteAccessAllowed)) { + return nullptr; + } + + const ObjectStoreTable& objectStores = mMetadata->mObjectStores; + const uint32_t nameCount = aObjectStoreNames.Length(); + + if (NS_WARN_IF(nameCount > objectStores.Count())) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + FallibleTArray<RefPtr<FullObjectStoreMetadata>> fallibleObjectStores; + if (NS_WARN_IF(!fallibleObjectStores.SetCapacity(nameCount, fallible))) { + return nullptr; + } + + for (uint32_t nameIndex = 0; nameIndex < nameCount; nameIndex++) { + const nsString& name = aObjectStoreNames[nameIndex]; + + if (nameIndex) { + // Make sure that this name is sorted properly and not a duplicate. + if (NS_WARN_IF(name <= aObjectStoreNames[nameIndex - 1])) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + } + + for (auto iter = objectStores.ConstIter(); !iter.Done(); iter.Next()) { + auto value = iter.Data(); + MOZ_ASSERT(iter.Key()); + + if (name == value->mCommonMetadata.name() && !value->mDeleted) { + if (NS_WARN_IF(!fallibleObjectStores.AppendElement(value, fallible))) { + return nullptr; + } + break; + } + } + } + + nsTArray<RefPtr<FullObjectStoreMetadata>> infallibleObjectStores; + infallibleObjectStores.SwapElements(fallibleObjectStores); + + RefPtr<NormalTransaction> transaction = + new NormalTransaction(this, aMode, infallibleObjectStores); + + MOZ_ASSERT(infallibleObjectStores.IsEmpty()); + + return transaction.forget().take(); +} + +bool +Database::RecvPBackgroundIDBTransactionConstructor( + PBackgroundIDBTransactionParent* aActor, + InfallibleTArray<nsString>&& aObjectStoreNames, + const Mode& aMode) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); + MOZ_ASSERT(aMode == IDBTransaction::READ_ONLY || + aMode == IDBTransaction::READ_WRITE || + aMode == IDBTransaction::READ_WRITE_FLUSH || + aMode == IDBTransaction::CLEANUP); + MOZ_ASSERT(!mClosed); + + if (IsInvalidated()) { + // This is an expected race. We don't want the child to die here, just don't + // actually do any work. + return true; + } + + if (!gConnectionPool) { + gConnectionPool = new ConnectionPool(); + } + + auto* transaction = static_cast<NormalTransaction*>(aActor); + + RefPtr<StartTransactionOp> startOp = new StartTransactionOp(transaction); + + uint64_t transactionId = + startOp->StartOnConnectionPool(GetLoggingInfo()->Id(), + mMetadata->mDatabaseId, + transaction->LoggingSerialNumber(), + aObjectStoreNames, + aMode != IDBTransaction::READ_ONLY); + + transaction->SetActive(transactionId); + + if (NS_WARN_IF(!RegisterTransaction(transaction))) { + IDB_REPORT_INTERNAL_ERR(); + transaction->Abort(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, /* aForce */ false); + return true; + } + + return true; +} + +bool +Database::DeallocPBackgroundIDBTransactionParent( + PBackgroundIDBTransactionParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<NormalTransaction> transaction = + dont_AddRef(static_cast<NormalTransaction*>(aActor)); + return true; +} + +PBackgroundIDBVersionChangeTransactionParent* +Database::AllocPBackgroundIDBVersionChangeTransactionParent( + const uint64_t& aCurrentVersion, + const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, + const int64_t& aNextIndexId) +{ + MOZ_CRASH("PBackgroundIDBVersionChangeTransactionParent actors should be " + "constructed manually!"); +} + +bool +Database::DeallocPBackgroundIDBVersionChangeTransactionParent( + PBackgroundIDBVersionChangeTransactionParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<VersionChangeTransaction> transaction = + dont_AddRef(static_cast<VersionChangeTransaction*>(aActor)); + return true; +} + +Database::PBackgroundMutableFileParent* +Database::AllocPBackgroundMutableFileParent(const nsString& aName, + const nsString& aType) +{ + MOZ_CRASH("PBackgroundMutableFileParent actors should be constructed " + "manually!"); +} + +bool +Database::DeallocPBackgroundMutableFileParent( + PBackgroundMutableFileParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<MutableFile> mutableFile = + dont_AddRef(static_cast<MutableFile*>(aActor)); + return true; +} + +bool +Database::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + return PBackgroundIDBDatabaseParent::Send__delete__(this); +} + +bool +Database::RecvBlocked() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mClosed)) { + return false; + } + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); + + MOZ_ASSERT(info->mLiveDatabases.Contains(this)); + MOZ_ASSERT(info->mWaitingFactoryOp); + + info->mWaitingFactoryOp->NoteDatabaseBlocked(this); + + return true; +} + +bool +Database::RecvClose() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!CloseInternal())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + return true; +} + +void +Database:: +StartTransactionOp::RunOnConnectionThread() +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(Transaction()); + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " + "Beginning database work", + "IndexedDB %s: P T[%lld]: DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mLoggingSerialNumber); + + TransactionDatabaseOperationBase::RunOnConnectionThread(); +} + +nsresult +Database:: +StartTransactionOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + Transaction()->SetActiveOnConnectionThread(); + + if (Transaction()->GetMode() == IDBTransaction::CLEANUP) { + nsresult rv = aConnection->DisableQuotaChecks(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (Transaction()->GetMode() != IDBTransaction::READ_ONLY) { + nsresult rv = aConnection->BeginWriteTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult +Database:: +StartTransactionOp::SendSuccessResult() +{ + // We don't need to do anything here. + return NS_OK; +} + +bool +Database:: +StartTransactionOp::SendFailureResult(nsresult /* aResultCode */) +{ + IDB_REPORT_INTERNAL_ERR(); + + // Abort the transaction. + return false; +} + +void +Database:: +StartTransactionOp::Cleanup() +{ +#ifdef DEBUG + // StartTransactionOp is not a normal database operation that is tied to an + // actor. Do this to make our assertions happy. + NoteActorDestroyed(); +#endif + + TransactionDatabaseOperationBase::Cleanup(); +} + +/******************************************************************************* + * TransactionBase + ******************************************************************************/ + +TransactionBase::TransactionBase(Database* aDatabase, Mode aMode) + : mDatabase(aDatabase) + , mTransactionId(0) + , mDatabaseId(aDatabase->Id()) + , mLoggingSerialNumber(aDatabase->GetLoggingInfo()->NextTransactionSN(aMode)) + , mActiveRequestCount(0) + , mInvalidatedOnAnyThread(false) + , mMode(aMode) + , mHasBeenActive(false) + , mHasBeenActiveOnConnectionThread(false) + , mActorDestroyed(false) + , mInvalidated(false) + , mResultCode(NS_OK) + , mCommitOrAbortReceived(false) + , mCommittedOrAborted(false) + , mForceAborted(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mLoggingSerialNumber); +} + +TransactionBase::~TransactionBase() +{ + MOZ_ASSERT(!mActiveRequestCount); + MOZ_ASSERT(mActorDestroyed); + MOZ_ASSERT_IF(mHasBeenActive, mCommittedOrAborted); +} + +void +TransactionBase::Abort(nsresult aResultCode, bool aForce) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aResultCode; + } + + if (aForce) { + mForceAborted = true; + } + + MaybeCommitOrAbort(); +} + +bool +TransactionBase::RecvCommit() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + mCommitOrAbortReceived = true; + + MaybeCommitOrAbort(); + return true; +} + +bool +TransactionBase::RecvAbort(nsresult aResultCode) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(NS_SUCCEEDED(aResultCode))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(NS_ERROR_GET_MODULE(aResultCode) != + NS_ERROR_MODULE_DOM_INDEXEDDB)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + mCommitOrAbortReceived = true; + + Abort(aResultCode, /* aForce */ false); + return true; +} + +void +TransactionBase::CommitOrAbort() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCommittedOrAborted); + + mCommittedOrAborted = true; + + if (!mHasBeenActive) { + return; + } + + RefPtr<CommitOp> commitOp = + new CommitOp(this, ClampResultCode(mResultCode)); + + gConnectionPool->Finish(TransactionId(), commitOp); +} + +already_AddRefed<FullObjectStoreMetadata> +TransactionBase::GetMetadataForObjectStoreId(int64_t aObjectStoreId) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aObjectStoreId); + + if (!aObjectStoreId) { + return nullptr; + } + + RefPtr<FullObjectStoreMetadata> metadata; + if (!mDatabase->Metadata()->mObjectStores.Get(aObjectStoreId, + getter_AddRefs(metadata)) || + metadata->mDeleted) { + return nullptr; + } + + MOZ_ASSERT(metadata->mCommonMetadata.id() == aObjectStoreId); + + return metadata.forget(); +} + +already_AddRefed<FullIndexMetadata> +TransactionBase::GetMetadataForIndexId( + FullObjectStoreMetadata* const aObjectStoreMetadata, + int64_t aIndexId) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aIndexId); + + if (!aIndexId) { + return nullptr; + } + + RefPtr<FullIndexMetadata> metadata; + if (!aObjectStoreMetadata->mIndexes.Get(aIndexId, getter_AddRefs(metadata)) || + metadata->mDeleted) { + return nullptr; + } + + MOZ_ASSERT(metadata->mCommonMetadata.id() == aIndexId); + + return metadata.forget(); +} + +void +TransactionBase::NoteModifiedAutoIncrementObjectStore( + FullObjectStoreMetadata* aMetadata) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aMetadata); + + if (!mModifiedAutoIncrementObjectStoreMetadataArray.Contains(aMetadata)) { + mModifiedAutoIncrementObjectStoreMetadataArray.AppendElement(aMetadata); + } +} + +void +TransactionBase::ForgetModifiedAutoIncrementObjectStore( + FullObjectStoreMetadata* aMetadata) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aMetadata); + + mModifiedAutoIncrementObjectStoreMetadataArray.RemoveElement(aMetadata); +} + +bool +TransactionBase::VerifyRequestParams(const RequestParams& aParams) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + switch (aParams.type()) { + case RequestParams::TObjectStoreAddParams: { + const ObjectStoreAddPutParams& params = + aParams.get_ObjectStoreAddParams().commonParams(); + if (NS_WARN_IF(!VerifyRequestParams(params))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStorePutParams: { + const ObjectStoreAddPutParams& params = + aParams.get_ObjectStorePutParams().commonParams(); + if (NS_WARN_IF(!VerifyRequestParams(params))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreGetParams: { + const ObjectStoreGetParams& params = aParams.get_ObjectStoreGetParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreGetKeyParams: { + const ObjectStoreGetKeyParams& params = + aParams.get_ObjectStoreGetKeyParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreGetAllParams: { + const ObjectStoreGetAllParams& params = + aParams.get_ObjectStoreGetAllParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreGetAllKeysParams: { + const ObjectStoreGetAllKeysParams& params = + aParams.get_ObjectStoreGetAllKeysParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreDeleteParams: { + if (NS_WARN_IF(mMode != IDBTransaction::READ_WRITE && + mMode != IDBTransaction::READ_WRITE_FLUSH && + mMode != IDBTransaction::CLEANUP && + mMode != IDBTransaction::VERSION_CHANGE)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const ObjectStoreDeleteParams& params = + aParams.get_ObjectStoreDeleteParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreClearParams: { + if (NS_WARN_IF(mMode != IDBTransaction::READ_WRITE && + mMode != IDBTransaction::READ_WRITE_FLUSH && + mMode != IDBTransaction::CLEANUP && + mMode != IDBTransaction::VERSION_CHANGE)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const ObjectStoreClearParams& params = + aParams.get_ObjectStoreClearParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TObjectStoreCountParams: { + const ObjectStoreCountParams& params = + aParams.get_ObjectStoreCountParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + + case RequestParams::TIndexGetParams: { + const IndexGetParams& params = aParams.get_IndexGetParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + const RefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TIndexGetKeyParams: { + const IndexGetKeyParams& params = aParams.get_IndexGetKeyParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + const RefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TIndexGetAllParams: { + const IndexGetAllParams& params = aParams.get_IndexGetAllParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + const RefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TIndexGetAllKeysParams: { + const IndexGetAllKeysParams& params = aParams.get_IndexGetAllKeysParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + const RefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + case RequestParams::TIndexCountParams: { + const IndexCountParams& params = aParams.get_IndexCountParams(); + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + const RefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +bool +TransactionBase::VerifyRequestParams(const SerializedKeyRange& aParams) const +{ + AssertIsOnBackgroundThread(); + + // XXX Check more here? + + if (aParams.isOnly()) { + if (NS_WARN_IF(aParams.lower().IsUnset())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!aParams.upper().IsUnset())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(aParams.lowerOpen())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(aParams.upperOpen())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + } else if (NS_WARN_IF(aParams.lower().IsUnset() && + aParams.upper().IsUnset())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + return true; +} + +bool +TransactionBase::VerifyRequestParams(const ObjectStoreAddPutParams& aParams) + const +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mMode != IDBTransaction::READ_WRITE && + mMode != IDBTransaction::READ_WRITE_FLUSH && + mMode != IDBTransaction::VERSION_CHANGE)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> objMetadata = + GetMetadataForObjectStoreId(aParams.objectStoreId()); + if (NS_WARN_IF(!objMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(!aParams.cloneInfo().data().data.Size())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (objMetadata->mCommonMetadata.autoIncrement() && + objMetadata->mCommonMetadata.keyPath().IsValid() && + aParams.key().IsUnset()) { + const SerializedStructuredCloneWriteInfo cloneInfo = aParams.cloneInfo(); + + if (NS_WARN_IF(!cloneInfo.offsetToKeyProp())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(cloneInfo.data().data.Size() < sizeof(uint64_t))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(cloneInfo.offsetToKeyProp() > + (cloneInfo.data().data.Size() - sizeof(uint64_t)))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + } else if (NS_WARN_IF(aParams.cloneInfo().offsetToKeyProp())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const nsTArray<IndexUpdateInfo>& updates = aParams.indexUpdateInfos(); + + for (uint32_t index = 0; index < updates.Length(); index++) { + RefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(objMetadata, updates[index].indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(updates[index].value().IsUnset())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + MOZ_ASSERT(!updates[index].value().GetBuffer().IsEmpty()); + } + + const nsTArray<FileAddInfo>& fileAddInfos = aParams.fileAddInfos(); + + for (uint32_t index = 0; index < fileAddInfos.Length(); index++) { + const FileAddInfo& fileAddInfo = fileAddInfos[index]; + + const DatabaseOrMutableFile& file = fileAddInfo.file(); + MOZ_ASSERT(file.type() != DatabaseOrMutableFile::T__None); + + switch (fileAddInfo.type()) { + case StructuredCloneFile::eBlob: + if (NS_WARN_IF(file.type() != + DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!file.get_PBackgroundIDBDatabaseFileParent())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + case StructuredCloneFile::eMutableFile: { + if (NS_WARN_IF(file.type() != + DatabaseOrMutableFile::TPBackgroundMutableFileParent)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mDatabase->IsFileHandleDisabled())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + auto mutableFile = + static_cast<MutableFile*>(file.get_PBackgroundMutableFileParent()); + + if (NS_WARN_IF(!mutableFile)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + Database* database = mutableFile->GetDatabase(); + if (NS_WARN_IF(!database)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(database->Id() != mDatabase->Id())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + break; + } + + case StructuredCloneFile::eStructuredClone: + ASSERT_UNLESS_FUZZING(); + return false; + + case StructuredCloneFile::eWasmBytecode: + case StructuredCloneFile::eWasmCompiled: + if (NS_WARN_IF(file.type() != + DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + if (NS_WARN_IF(!file.get_PBackgroundIDBDatabaseFileParent())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + case StructuredCloneFile::eEndGuard: + ASSERT_UNLESS_FUZZING(); + return false; + + default: + MOZ_CRASH("Should never get here!"); + } + } + + return true; +} + +bool +TransactionBase::VerifyRequestParams(const OptionalKeyRange& aParams) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != OptionalKeyRange::T__None); + + switch (aParams.type()) { + case OptionalKeyRange::TSerializedKeyRange: + if (NS_WARN_IF(!VerifyRequestParams(aParams.get_SerializedKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + case OptionalKeyRange::Tvoid_t: + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +void +TransactionBase::NoteActiveRequest() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActiveRequestCount < UINT64_MAX); + + mActiveRequestCount++; +} + +void +TransactionBase::NoteFinishedRequest() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActiveRequestCount); + + mActiveRequestCount--; + + MaybeCommitOrAbort(); +} + +void +TransactionBase::Invalidate() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mInvalidated == mInvalidatedOnAnyThread); + + if (!mInvalidated) { + mInvalidated = true; + mInvalidatedOnAnyThread = true; + + Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, /* aForce */ false); + } +} + +PBackgroundIDBRequestParent* +TransactionBase::AllocRequest(const RequestParams& aParams, bool aTrustParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + aTrustParams = false; +#endif + + if (!aTrustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr<NormalTransactionOp> actor; + + switch (aParams.type()) { + case RequestParams::TObjectStoreAddParams: + case RequestParams::TObjectStorePutParams: + actor = new ObjectStoreAddOrPutRequestOp(this, aParams); + break; + + case RequestParams::TObjectStoreGetParams: + actor = + new ObjectStoreGetRequestOp(this, aParams, /* aGetAll */ false); + break; + + case RequestParams::TObjectStoreGetAllParams: + actor = + new ObjectStoreGetRequestOp(this, aParams, /* aGetAll */ true); + break; + + case RequestParams::TObjectStoreGetKeyParams: + actor = + new ObjectStoreGetKeyRequestOp(this, aParams, /* aGetAll */ false); + break; + + case RequestParams::TObjectStoreGetAllKeysParams: + actor = + new ObjectStoreGetKeyRequestOp(this, aParams, /* aGetAll */ true); + break; + + case RequestParams::TObjectStoreDeleteParams: + actor = + new ObjectStoreDeleteRequestOp(this, + aParams.get_ObjectStoreDeleteParams()); + break; + + case RequestParams::TObjectStoreClearParams: + actor = + new ObjectStoreClearRequestOp(this, + aParams.get_ObjectStoreClearParams()); + break; + + case RequestParams::TObjectStoreCountParams: + actor = + new ObjectStoreCountRequestOp(this, + aParams.get_ObjectStoreCountParams()); + break; + + case RequestParams::TIndexGetParams: + actor = new IndexGetRequestOp(this, aParams, /* aGetAll */ false); + break; + + case RequestParams::TIndexGetKeyParams: + actor = new IndexGetKeyRequestOp(this, aParams, /* aGetAll */ false); + break; + + case RequestParams::TIndexGetAllParams: + actor = new IndexGetRequestOp(this, aParams, /* aGetAll */ true); + break; + + case RequestParams::TIndexGetAllKeysParams: + actor = new IndexGetKeyRequestOp(this, aParams, /* aGetAll */ true); + break; + + case RequestParams::TIndexCountParams: + actor = new IndexCountRequestOp(this, aParams); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + MOZ_ASSERT(actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool +TransactionBase::StartRequest(PBackgroundIDBRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + auto* op = static_cast<NormalTransactionOp*>(aActor); + + if (NS_WARN_IF(!op->Init(this))) { + op->Cleanup(); + return false; + } + + op->DispatchToConnectionPool(); + return true; +} + +bool +TransactionBase::DeallocRequest(PBackgroundIDBRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<NormalTransactionOp> actor = + dont_AddRef(static_cast<NormalTransactionOp*>(aActor)); + return true; +} + +PBackgroundIDBCursorParent* +TransactionBase::AllocCursor(const OpenCursorParams& aParams, bool aTrustParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + aTrustParams = false; +#endif + + OpenCursorParams::Type type = aParams.type(); + RefPtr<FullObjectStoreMetadata> objectStoreMetadata; + RefPtr<FullIndexMetadata> indexMetadata; + Cursor::Direction direction; + + switch (type) { + case OpenCursorParams::TObjectStoreOpenCursorParams: { + const ObjectStoreOpenCursorParams& params = + aParams.get_ObjectStoreOpenCursorParams(); + objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + if (aTrustParams && + NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + direction = params.direction(); + break; + } + + case OpenCursorParams::TObjectStoreOpenKeyCursorParams: { + const ObjectStoreOpenKeyCursorParams& params = + aParams.get_ObjectStoreOpenKeyCursorParams(); + objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + if (aTrustParams && + NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + direction = params.direction(); + break; + } + + case OpenCursorParams::TIndexOpenCursorParams: { + const IndexOpenCursorParams& params = aParams.get_IndexOpenCursorParams(); + objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + if (aTrustParams && + NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + direction = params.direction(); + break; + } + + case OpenCursorParams::TIndexOpenKeyCursorParams: { + const IndexOpenKeyCursorParams& params = + aParams.get_IndexOpenKeyCursorParams(); + objectStoreMetadata = GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_WARN_IF(!objectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + indexMetadata = + GetMetadataForIndexId(objectStoreMetadata, params.indexId()); + if (NS_WARN_IF(!indexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + if (aTrustParams && + NS_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + direction = params.direction(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr<Cursor> actor = + new Cursor(this, type, objectStoreMetadata, indexMetadata, direction); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +bool +TransactionBase::StartCursor(PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + auto* op = static_cast<Cursor*>(aActor); + + if (NS_WARN_IF(!op->Start(aParams))) { + return false; + } + + return true; +} + +bool +TransactionBase::DeallocCursor(PBackgroundIDBCursorParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<Cursor> actor = dont_AddRef(static_cast<Cursor*>(aActor)); + return true; +} + +/******************************************************************************* + * NormalTransaction + ******************************************************************************/ + +NormalTransaction::NormalTransaction( + Database* aDatabase, + TransactionBase::Mode aMode, + nsTArray<RefPtr<FullObjectStoreMetadata>>& aObjectStores) + : TransactionBase(aDatabase, aMode) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!aObjectStores.IsEmpty()); + + mObjectStores.SwapElements(aObjectStores); +} + +bool +NormalTransaction::IsSameProcessActor() +{ + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager()->Manager()->Manager(); + MOZ_ASSERT(actor); + + return !BackgroundParent::IsOtherProcessActor(actor); +} + +void +NormalTransaction::SendCompleteNotification(nsresult aResult) +{ + AssertIsOnBackgroundThread(); + + if (!IsActorDestroyed()) { + Unused << SendComplete(aResult); + } +} + +void +NormalTransaction::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + + NoteActorDestroyed(); + + if (!mCommittedOrAborted) { + if (NS_SUCCEEDED(mResultCode)) { + IDB_REPORT_INTERNAL_ERR(); + mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mForceAborted = true; + + MaybeCommitOrAbort(); + } +} + +bool +NormalTransaction::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return PBackgroundIDBTransactionParent::Send__delete__(this); +} + +bool +NormalTransaction::RecvCommit() +{ + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvCommit(); +} + +bool +NormalTransaction::RecvAbort(const nsresult& aResultCode) +{ + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvAbort(aResultCode); +} + +PBackgroundIDBRequestParent* +NormalTransaction::AllocPBackgroundIDBRequestParent( + const RequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + return AllocRequest(aParams, IsSameProcessActor()); +} + +bool +NormalTransaction::RecvPBackgroundIDBRequestConstructor( + PBackgroundIDBRequestParent* aActor, + const RequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + return StartRequest(aActor); +} + +bool +NormalTransaction::DeallocPBackgroundIDBRequestParent( + PBackgroundIDBRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return DeallocRequest(aActor); +} + +PBackgroundIDBCursorParent* +NormalTransaction::AllocPBackgroundIDBCursorParent( + const OpenCursorParams& aParams) +{ + AssertIsOnBackgroundThread(); + + return AllocCursor(aParams, IsSameProcessActor()); +} + +bool +NormalTransaction::RecvPBackgroundIDBCursorConstructor( + PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + return StartCursor(aActor, aParams); +} + +bool +NormalTransaction::DeallocPBackgroundIDBCursorParent( + PBackgroundIDBCursorParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return DeallocCursor(aActor); +} + +/******************************************************************************* + * VersionChangeTransaction + ******************************************************************************/ + +VersionChangeTransaction::VersionChangeTransaction( + OpenDatabaseOp* aOpenDatabaseOp) + : TransactionBase(aOpenDatabaseOp->mDatabase, + IDBTransaction::VERSION_CHANGE) + , mOpenDatabaseOp(aOpenDatabaseOp) + , mActorWasAlive(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aOpenDatabaseOp); +} + +VersionChangeTransaction::~VersionChangeTransaction() +{ +#ifdef DEBUG + // Silence the base class' destructor assertion if we never made this actor + // live. + FakeActorDestroyed(); +#endif +} + +bool +VersionChangeTransaction::IsSameProcessActor() +{ + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager()->Manager()->Manager(); + MOZ_ASSERT(actor); + + return !BackgroundParent::IsOtherProcessActor(actor); +} + +void +VersionChangeTransaction::SetActorAlive() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorWasAlive); + MOZ_ASSERT(!IsActorDestroyed()); + + mActorWasAlive = true; + + // This reference will be absorbed by IPDL and released when the actor is + // destroyed. + AddRef(); +} + +bool +VersionChangeTransaction::CopyDatabaseMetadata() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mOldMetadata); + + const RefPtr<FullDatabaseMetadata> origMetadata = + GetDatabase()->Metadata(); + MOZ_ASSERT(origMetadata); + + RefPtr<FullDatabaseMetadata> newMetadata = origMetadata->Duplicate(); + if (NS_WARN_IF(!newMetadata)) { + return false; + } + + // Replace the live metadata with the new mutable copy. + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(origMetadata->mDatabaseId, + &info)); + MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); + MOZ_ASSERT(info->mMetadata == origMetadata); + + mOldMetadata = info->mMetadata.forget(); + info->mMetadata.swap(newMetadata); + + // Replace metadata pointers for all live databases. + for (uint32_t count = info->mLiveDatabases.Length(), index = 0; + index < count; + index++) { + info->mLiveDatabases[index]->mMetadata = info->mMetadata; + } + + return true; +} + +void +VersionChangeTransaction::UpdateMetadata(nsresult aResult) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(GetDatabase()); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT(!!mActorWasAlive == !!mOpenDatabaseOp->mDatabase); + MOZ_ASSERT_IF(mActorWasAlive, !mOpenDatabaseOp->mDatabaseId.IsEmpty()); + + if (IsActorDestroyed() || !mActorWasAlive) { + return; + } + + RefPtr<FullDatabaseMetadata> oldMetadata; + mOldMetadata.swap(oldMetadata); + + DatabaseActorInfo* info; + if (!gLiveDatabaseHashtable->Get(oldMetadata->mDatabaseId, &info)) { + return; + } + + MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); + + if (NS_SUCCEEDED(aResult)) { + // Remove all deleted objectStores and indexes, then mark immutable. + for (auto objectStoreIter = info->mMetadata->mObjectStores.Iter(); + !objectStoreIter.Done(); + objectStoreIter.Next()) { + MOZ_ASSERT(objectStoreIter.Key()); + RefPtr<FullObjectStoreMetadata>& metadata = objectStoreIter.Data(); + MOZ_ASSERT(metadata); + + if (metadata->mDeleted) { + objectStoreIter.Remove(); + continue; + } + + for (auto indexIter = metadata->mIndexes.Iter(); + !indexIter.Done(); + indexIter.Next()) { + MOZ_ASSERT(indexIter.Key()); + RefPtr<FullIndexMetadata>& index = indexIter.Data(); + MOZ_ASSERT(index); + + if (index->mDeleted) { + indexIter.Remove(); + } + } +#ifdef DEBUG + metadata->mIndexes.MarkImmutable(); +#endif + } +#ifdef DEBUG + info->mMetadata->mObjectStores.MarkImmutable(); +#endif + } else { + // Replace metadata pointers for all live databases. + info->mMetadata = oldMetadata.forget(); + + for (uint32_t count = info->mLiveDatabases.Length(), index = 0; + index < count; + index++) { + info->mLiveDatabases[index]->mMetadata = info->mMetadata; + } + } +} + +void +VersionChangeTransaction::SendCompleteNotification(nsresult aResult) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT_IF(!mActorWasAlive, NS_FAILED(mOpenDatabaseOp->mResultCode)); + MOZ_ASSERT_IF(!mActorWasAlive, + mOpenDatabaseOp->mState > OpenDatabaseOp::State::SendingResults); + + RefPtr<OpenDatabaseOp> openDatabaseOp; + mOpenDatabaseOp.swap(openDatabaseOp); + + if (!mActorWasAlive) { + return; + } + + if (NS_FAILED(aResult) && NS_SUCCEEDED(openDatabaseOp->mResultCode)) { + // 3.3.1 Opening a database: + // "If the upgrade transaction was aborted, run the steps for closing a + // database connection with connection, create and return a new AbortError + // exception and abort these steps." + openDatabaseOp->mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } + + openDatabaseOp->mState = OpenDatabaseOp::State::SendingResults; + + if (!IsActorDestroyed()) { + Unused << SendComplete(aResult); + } + + MOZ_ALWAYS_SUCCEEDS(openDatabaseOp->Run()); +} + +void +VersionChangeTransaction::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + + NoteActorDestroyed(); + + if (!mCommittedOrAborted) { + if (NS_SUCCEEDED(mResultCode)) { + IDB_REPORT_INTERNAL_ERR(); + mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mForceAborted = true; + + MaybeCommitOrAbort(); + } +} + +bool +VersionChangeTransaction::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return PBackgroundIDBVersionChangeTransactionParent::Send__delete__(this); +} + +bool +VersionChangeTransaction::RecvCommit() +{ + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvCommit(); +} + +bool +VersionChangeTransaction::RecvAbort(const nsresult& aResultCode) +{ + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvAbort(aResultCode); +} + +bool +VersionChangeTransaction::RecvCreateObjectStore( + const ObjectStoreMetadata& aMetadata) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aMetadata.id())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); + MOZ_ASSERT(dbMetadata); + + if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + auto* foundMetadata = + MetadataNameOrIdMatcher<FullObjectStoreMetadata>::Match( + dbMetadata->mObjectStores, aMetadata.id(), aMetadata.name()); + + if (NS_WARN_IF(foundMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> newMetadata = new FullObjectStoreMetadata(); + newMetadata->mCommonMetadata = aMetadata; + newMetadata->mNextAutoIncrementId = aMetadata.autoIncrement() ? 1 : 0; + newMetadata->mCommittedAutoIncrementId = newMetadata->mNextAutoIncrementId; + + if (NS_WARN_IF(!dbMetadata->mObjectStores.Put(aMetadata.id(), newMetadata, + fallible))) { + return false; + } + + dbMetadata->mNextObjectStoreId++; + + RefPtr<CreateObjectStoreOp> op = new CreateObjectStoreOp(this, aMetadata); + + if (NS_WARN_IF(!op->Init(this))) { + op->Cleanup(); + return false; + } + + op->DispatchToConnectionPool(); + + return true; +} + +bool +VersionChangeTransaction::RecvDeleteObjectStore(const int64_t& aObjectStoreId) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); + MOZ_ASSERT(dbMetadata); + MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> foundMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + foundMetadata->mDeleted = true; + + bool isLastObjectStore = true; + DebugOnly<bool> foundTargetId = false; + for (auto iter = dbMetadata->mObjectStores.Iter(); + !iter.Done(); + iter.Next()) { + if (uint64_t(aObjectStoreId) == iter.Key()) { + foundTargetId = true; + } else if (!iter.UserData()->mDeleted) { + isLastObjectStore = false; + break; + } + } + MOZ_ASSERT_IF(isLastObjectStore, foundTargetId); + + RefPtr<DeleteObjectStoreOp> op = + new DeleteObjectStoreOp(this, foundMetadata, isLastObjectStore); + + if (NS_WARN_IF(!op->Init(this))) { + op->Cleanup(); + return false; + } + + op->DispatchToConnectionPool(); + + return true; +} + +bool +VersionChangeTransaction::RecvRenameObjectStore(const int64_t& aObjectStoreId, + const nsString& aName) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); + MOZ_ASSERT(dbMetadata); + MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> foundMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + foundMetadata->mCommonMetadata.name() = aName; + + RefPtr<RenameObjectStoreOp> renameOp = + new RenameObjectStoreOp(this, foundMetadata); + + if (NS_WARN_IF(!renameOp->Init(this))) { + renameOp->Cleanup(); + return false; + } + + renameOp->DispatchToConnectionPool(); + + return true; +} + +bool +VersionChangeTransaction::RecvCreateIndex(const int64_t& aObjectStoreId, + const IndexMetadata& aMetadata) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(!aMetadata.id())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); + MOZ_ASSERT(dbMetadata); + + if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextIndexId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundObjectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullIndexMetadata> foundIndexMetadata = + MetadataNameOrIdMatcher<FullIndexMetadata>::Match( + foundObjectStoreMetadata->mIndexes, aMetadata.id(), aMetadata.name()); + + if (NS_WARN_IF(foundIndexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullIndexMetadata> newMetadata = new FullIndexMetadata(); + newMetadata->mCommonMetadata = aMetadata; + + if (NS_WARN_IF(!foundObjectStoreMetadata->mIndexes.Put(aMetadata.id(), + newMetadata, + fallible))) { + return false; + } + + dbMetadata->mNextIndexId++; + + RefPtr<CreateIndexOp> op = + new CreateIndexOp(this, aObjectStoreId, aMetadata); + + if (NS_WARN_IF(!op->Init(this))) { + op->Cleanup(); + return false; + } + + op->DispatchToConnectionPool(); + + return true; +} + +bool +VersionChangeTransaction::RecvDeleteIndex(const int64_t& aObjectStoreId, + const int64_t& aIndexId) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(!aIndexId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); + MOZ_ASSERT(dbMetadata); + MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); + MOZ_ASSERT(dbMetadata->mNextIndexId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(aIndexId >= dbMetadata->mNextIndexId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundObjectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullIndexMetadata> foundIndexMetadata = + GetMetadataForIndexId(foundObjectStoreMetadata, aIndexId); + + if (NS_WARN_IF(!foundIndexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + foundIndexMetadata->mDeleted = true; + + bool isLastIndex = true; + DebugOnly<bool> foundTargetId = false; + for (auto iter = foundObjectStoreMetadata->mIndexes.ConstIter(); + !iter.Done(); + iter.Next()) { + if (uint64_t(aIndexId) == iter.Key()) { + foundTargetId = true; + } else if (!iter.UserData()->mDeleted) { + isLastIndex = false; + break; + } + } + MOZ_ASSERT_IF(isLastIndex, foundTargetId); + + RefPtr<DeleteIndexOp> op = + new DeleteIndexOp(this, + aObjectStoreId, + aIndexId, + foundIndexMetadata->mCommonMetadata.unique(), + isLastIndex); + + if (NS_WARN_IF(!op->Init(this))) { + op->Cleanup(); + return false; + } + + op->DispatchToConnectionPool(); + + return true; +} + +bool +VersionChangeTransaction::RecvRenameIndex(const int64_t& aObjectStoreId, + const int64_t& aIndexId, + const nsString& aName) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(!aIndexId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const RefPtr<FullDatabaseMetadata> dbMetadata = GetDatabase()->Metadata(); + MOZ_ASSERT(dbMetadata); + MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); + MOZ_ASSERT(dbMetadata->mNextIndexId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(aIndexId >= dbMetadata->mNextIndexId)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundObjectStoreMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<FullIndexMetadata> foundIndexMetadata = + GetMetadataForIndexId(foundObjectStoreMetadata, aIndexId); + + if (NS_WARN_IF(!foundIndexMetadata)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + foundIndexMetadata->mCommonMetadata.name() = aName; + + RefPtr<RenameIndexOp> renameOp = + new RenameIndexOp(this, foundIndexMetadata, aObjectStoreId); + + if (NS_WARN_IF(!renameOp->Init(this))) { + renameOp->Cleanup(); + return false; + } + + renameOp->DispatchToConnectionPool(); + + return true; +} + +PBackgroundIDBRequestParent* +VersionChangeTransaction::AllocPBackgroundIDBRequestParent( + const RequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + return AllocRequest(aParams, IsSameProcessActor()); +} + +bool +VersionChangeTransaction::RecvPBackgroundIDBRequestConstructor( + PBackgroundIDBRequestParent* aActor, + const RequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + return StartRequest(aActor); +} + +bool +VersionChangeTransaction::DeallocPBackgroundIDBRequestParent( + PBackgroundIDBRequestParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return DeallocRequest(aActor); +} + +PBackgroundIDBCursorParent* +VersionChangeTransaction::AllocPBackgroundIDBCursorParent( + const OpenCursorParams& aParams) +{ + AssertIsOnBackgroundThread(); + + return AllocCursor(aParams, IsSameProcessActor()); +} + +bool +VersionChangeTransaction::RecvPBackgroundIDBCursorConstructor( + PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + return StartCursor(aActor, aParams); +} + +bool +VersionChangeTransaction::DeallocPBackgroundIDBCursorParent( + PBackgroundIDBCursorParent* aActor) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return DeallocCursor(aActor); +} + +/******************************************************************************* + * Cursor + ******************************************************************************/ + +Cursor::Cursor(TransactionBase* aTransaction, + Type aType, + FullObjectStoreMetadata* aObjectStoreMetadata, + FullIndexMetadata* aIndexMetadata, + Direction aDirection) + : mTransaction(aTransaction) + , mBackgroundParent(nullptr) + , mObjectStoreMetadata(aObjectStoreMetadata) + , mIndexMetadata(aIndexMetadata) + , mObjectStoreId(aObjectStoreMetadata->mCommonMetadata.id()) + , mIndexId(aIndexMetadata ? aIndexMetadata->mCommonMetadata.id() : 0) + , mCurrentlyRunningOp(nullptr) + , mType(aType) + , mDirection(aDirection) + , mUniqueIndex(aIndexMetadata ? + aIndexMetadata->mCommonMetadata.unique() : + false) + , mIsSameProcessActor(!BackgroundParent::IsOtherProcessActor( + aTransaction->GetBackgroundParent())) + , mActorDestroyed(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(aType != OpenCursorParams::T__None); + MOZ_ASSERT(aObjectStoreMetadata); + MOZ_ASSERT_IF(aType == OpenCursorParams::TIndexOpenCursorParams || + aType == OpenCursorParams::TIndexOpenKeyCursorParams, + aIndexMetadata); + + if (mType == OpenCursorParams::TObjectStoreOpenCursorParams || + mType == OpenCursorParams::TIndexOpenCursorParams) { + mDatabase = aTransaction->GetDatabase(); + MOZ_ASSERT(mDatabase); + + mFileManager = mDatabase->GetFileManager(); + MOZ_ASSERT(mFileManager); + + mBackgroundParent = aTransaction->GetBackgroundParent(); + MOZ_ASSERT(mBackgroundParent); + } + + if (aIndexMetadata) { + mLocale = aIndexMetadata->mCommonMetadata.locale(); + } + + static_assert(OpenCursorParams::T__None == 0 && + OpenCursorParams::T__Last == 4, + "Lots of code here assumes only four types of cursors!"); +} + +bool +Cursor::VerifyRequestParams(const CursorRequestParams& aParams) const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); + MOZ_ASSERT(mObjectStoreMetadata); + MOZ_ASSERT_IF(mType == OpenCursorParams::TIndexOpenCursorParams || + mType == OpenCursorParams::TIndexOpenKeyCursorParams, + mIndexMetadata); + +#ifdef DEBUG + { + RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + mTransaction->GetMetadataForObjectStoreId(mObjectStoreId); + if (objectStoreMetadata) { + MOZ_ASSERT(objectStoreMetadata == mObjectStoreMetadata); + } else { + MOZ_ASSERT(mObjectStoreMetadata->mDeleted); + } + + if (objectStoreMetadata && + (mType == OpenCursorParams::TIndexOpenCursorParams || + mType == OpenCursorParams::TIndexOpenKeyCursorParams)) { + RefPtr<FullIndexMetadata> indexMetadata = + mTransaction->GetMetadataForIndexId(objectStoreMetadata, mIndexId); + if (indexMetadata) { + MOZ_ASSERT(indexMetadata == mIndexMetadata); + } else { + MOZ_ASSERT(mIndexMetadata->mDeleted); + } + } + } +#endif + + if (NS_WARN_IF(mObjectStoreMetadata->mDeleted) || + (mIndexMetadata && NS_WARN_IF(mIndexMetadata->mDeleted))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const Key& sortKey = IsLocaleAware() ? mSortKey : mKey; + + switch (aParams.type()) { + case CursorRequestParams::TContinueParams: { + const Key& key = aParams.get_ContinueParams().key(); + if (!key.IsUnset()) { + switch (mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: + if (NS_WARN_IF(key <= sortKey)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + case IDBCursor::PREV: + case IDBCursor::PREV_UNIQUE: + if (NS_WARN_IF(key >= sortKey)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + } + break; + } + + case CursorRequestParams::TContinuePrimaryKeyParams: { + const Key& key = aParams.get_ContinuePrimaryKeyParams().key(); + const Key& primaryKey = aParams.get_ContinuePrimaryKeyParams().primaryKey(); + MOZ_ASSERT(!key.IsUnset()); + MOZ_ASSERT(!primaryKey.IsUnset()); + switch (mDirection) { + case IDBCursor::NEXT: + if (NS_WARN_IF(key < sortKey || + (key == sortKey && primaryKey <= mObjectKey))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + case IDBCursor::PREV: + if (NS_WARN_IF(key > sortKey || + (key == sortKey && primaryKey >= mObjectKey))) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + break; + } + + case CursorRequestParams::TAdvanceParams: + if (NS_WARN_IF(!aParams.get_AdvanceParams().count())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +bool +Cursor::Start(const OpenCursorParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() == mType); + MOZ_ASSERT(!mActorDestroyed); + + if (NS_WARN_IF(mCurrentlyRunningOp)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + const OptionalKeyRange& optionalKeyRange = + mType == OpenCursorParams::TObjectStoreOpenCursorParams ? + aParams.get_ObjectStoreOpenCursorParams().optionalKeyRange() : + mType == OpenCursorParams::TObjectStoreOpenKeyCursorParams ? + aParams.get_ObjectStoreOpenKeyCursorParams().optionalKeyRange() : + mType == OpenCursorParams::TIndexOpenCursorParams ? + aParams.get_IndexOpenCursorParams().optionalKeyRange() : + aParams.get_IndexOpenKeyCursorParams().optionalKeyRange(); + + RefPtr<OpenOp> openOp = new OpenOp(this, optionalKeyRange); + + if (NS_WARN_IF(!openOp->Init(mTransaction))) { + openOp->Cleanup(); + return false; + } + + openOp->DispatchToConnectionPool(); + mCurrentlyRunningOp = openOp; + + return true; +} + +void +Cursor::SendResponseInternal( + CursorResponse& aResponse, + const nsTArray<FallibleTArray<StructuredCloneFile>>& aFiles) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aResponse.type() != CursorResponse::T__None); + MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tnsresult, + NS_FAILED(aResponse.get_nsresult())); + MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tnsresult, + NS_ERROR_GET_MODULE(aResponse.get_nsresult()) == + NS_ERROR_MODULE_DOM_INDEXEDDB); + MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tvoid_t, mKey.IsUnset()); + MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tvoid_t, + mRangeKey.IsUnset()); + MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tvoid_t, + mObjectKey.IsUnset()); + MOZ_ASSERT_IF(aResponse.type() == CursorResponse::Tnsresult || + aResponse.type() == CursorResponse::Tvoid_t || + aResponse.type() == + CursorResponse::TObjectStoreKeyCursorResponse || + aResponse.type() == CursorResponse::TIndexKeyCursorResponse, + aFiles.IsEmpty()); + MOZ_ASSERT(!mActorDestroyed); + MOZ_ASSERT(mCurrentlyRunningOp); + + for (size_t i = 0; i < aFiles.Length(); ++i) { + const auto& files = aFiles[i]; + if (!files.IsEmpty()) { + MOZ_ASSERT(aResponse.type() == + CursorResponse::TArrayOfObjectStoreCursorResponse || + aResponse.type() == CursorResponse::TIndexCursorResponse); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mBackgroundParent); + + FallibleTArray<SerializedStructuredCloneFile> serializedFiles; + nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, + mDatabase, + files, + /* aForPreprocess */ false, + serializedFiles); + if (NS_WARN_IF(NS_FAILED(rv))) { + aResponse = ClampResultCode(rv); + break; + } + + SerializedStructuredCloneReadInfo* serializedInfo = nullptr; + switch (aResponse.type()) { + case CursorResponse::TArrayOfObjectStoreCursorResponse: { + auto& responses = aResponse.get_ArrayOfObjectStoreCursorResponse(); + MOZ_ASSERT(i < responses.Length()); + serializedInfo = &responses[i].cloneInfo(); + break; + } + + case CursorResponse::TIndexCursorResponse: + MOZ_ASSERT(i == 0); + serializedInfo = &aResponse.get_IndexCursorResponse().cloneInfo(); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + MOZ_ASSERT(serializedInfo); + MOZ_ASSERT(serializedInfo->files().IsEmpty()); + + serializedInfo->files().SwapElements(serializedFiles); + } + } + + // Work around the deleted function by casting to the base class. + auto* base = static_cast<PBackgroundIDBCursorParent*>(this); + if (!base->SendResponse(aResponse)) { + NS_WARNING("Failed to send response!"); + } + + mCurrentlyRunningOp = nullptr; +} + +void +Cursor::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; + + if (mCurrentlyRunningOp) { + mCurrentlyRunningOp->NoteActorDestroyed(); + } + + mBackgroundParent = nullptr; + + mObjectStoreMetadata = nullptr; + mIndexMetadata = nullptr; +} + +bool +Cursor::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + if (NS_WARN_IF(mCurrentlyRunningOp)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + return PBackgroundIDBCursorParent::Send__delete__(this); +} + +bool +Cursor::RecvContinue(const CursorRequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); + MOZ_ASSERT(!mActorDestroyed); + MOZ_ASSERT(mObjectStoreMetadata); + MOZ_ASSERT_IF(mType == OpenCursorParams::TIndexOpenCursorParams || + mType == OpenCursorParams::TIndexOpenKeyCursorParams, + mIndexMetadata); + + const bool trustParams = +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + false +#else + mIsSameProcessActor +#endif + ; + + if (!trustParams && !VerifyRequestParams(aParams)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mCurrentlyRunningOp)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(mTransaction->mCommitOrAbortReceived)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<ContinueOp> continueOp = new ContinueOp(this, aParams); + if (NS_WARN_IF(!continueOp->Init(mTransaction))) { + continueOp->Cleanup(); + return false; + } + + continueOp->DispatchToConnectionPool(); + mCurrentlyRunningOp = continueOp; + + return true; +} + +/******************************************************************************* + * FileManager + ******************************************************************************/ + +FileManager::FileManager(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + bool aIsApp, + const nsAString& aDatabaseName, + bool aEnforcingQuota) + : mPersistenceType(aPersistenceType) + , mGroup(aGroup) + , mOrigin(aOrigin) + , mDatabaseName(aDatabaseName) + , mLastFileId(0) + , mIsApp(aIsApp) + , mEnforcingQuota(aEnforcingQuota) + , mInvalidated(false) +{ } + +FileManager::~FileManager() +{ } + +nsresult +FileManager::Init(nsIFile* aDirectory, + mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(aConnection); + + bool exists; + nsresult rv = aDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + bool isDirectory; + rv = aDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + return NS_ERROR_FAILURE; + } + } else { + rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = aDirectory->GetPath(mDirectoryPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> journalDirectory; + rv = aDirectory->Clone(getter_AddRefs(journalDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = journalDirectory->Append(NS_LITERAL_STRING(JOURNAL_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = journalDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + bool isDirectory; + rv = journalDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + return NS_ERROR_FAILURE; + } + } + + rv = journalDirectory->GetPath(mJournalDirectoryPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, refcount " + "FROM file" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + int64_t id; + rv = stmt->GetInt64(0, &id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t refcount; + rv = stmt->GetInt32(1, &refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(refcount > 0); + + RefPtr<FileInfo> fileInfo = FileInfo::Create(this, id); + fileInfo->mDBRefCnt = static_cast<nsrefcnt>(refcount); + + mFileInfos.Put(id, fileInfo); + + mLastFileId = std::max(id, mLastFileId); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +FileManager::Invalidate() +{ + if (IndexedDatabaseManager::IsClosed()) { + MOZ_ASSERT(false, "Shouldn't be called after shutdown!"); + return NS_ERROR_UNEXPECTED; + } + + MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); + + MOZ_ASSERT(!mInvalidated); + mInvalidated = true; + + for (auto iter = mFileInfos.Iter(); !iter.Done(); iter.Next()) { + FileInfo* info = iter.Data(); + MOZ_ASSERT(info); + + if (!info->LockedClearDBRefs()) { + iter.Remove(); + } + } + + return NS_OK; +} + +already_AddRefed<nsIFile> +FileManager::GetDirectory() +{ + return GetFileForPath(mDirectoryPath); +} + +already_AddRefed<nsIFile> +FileManager::GetCheckedDirectory() +{ + nsCOMPtr<nsIFile> directory = GetDirectory(); + if (NS_WARN_IF(!directory)) { + return nullptr; + } + + DebugOnly<bool> exists; + MOZ_ASSERT(NS_SUCCEEDED(directory->Exists(&exists))); + MOZ_ASSERT(exists); + + DebugOnly<bool> isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(directory->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory); + + return directory.forget(); +} + +already_AddRefed<nsIFile> +FileManager::GetJournalDirectory() +{ + return GetFileForPath(mJournalDirectoryPath); +} + +already_AddRefed<nsIFile> +FileManager::EnsureJournalDirectory() +{ + // This can happen on the IO or on a transaction thread. + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> journalDirectory = GetFileForPath(mJournalDirectoryPath); + if (NS_WARN_IF(!journalDirectory)) { + return nullptr; + } + + bool exists; + nsresult rv = journalDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + if (exists) { + bool isDirectory; + rv = journalDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + if (NS_WARN_IF(!isDirectory)) { + return nullptr; + } + } else { + rv = journalDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + } + + return journalDirectory.forget(); +} + +already_AddRefed<FileInfo> +FileManager::GetFileInfo(int64_t aId) +{ + if (IndexedDatabaseManager::IsClosed()) { + MOZ_ASSERT(false, "Shouldn't be called after shutdown!"); + return nullptr; + } + + FileInfo* fileInfo; + { + MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); + fileInfo = mFileInfos.Get(aId); + } + + RefPtr<FileInfo> result = fileInfo; + return result.forget(); +} + +already_AddRefed<FileInfo> +FileManager::GetNewFileInfo() +{ + MOZ_ASSERT(!IndexedDatabaseManager::IsClosed()); + + FileInfo* fileInfo; + { + MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); + + int64_t id = mLastFileId + 1; + + fileInfo = FileInfo::Create(this, id); + + mFileInfos.Put(id, fileInfo); + + mLastFileId = id; + } + + RefPtr<FileInfo> result = fileInfo; + return result.forget(); +} + +// static +already_AddRefed<nsIFile> +FileManager::GetFileForId(nsIFile* aDirectory, int64_t aId) +{ + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(aId > 0); + + nsAutoString id; + id.AppendInt(aId); + + nsCOMPtr<nsIFile> file; + nsresult rv = aDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + rv = file->Append(id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return file.forget(); +} + +// static +already_AddRefed<nsIFile> +FileManager::GetCheckedFileForId(nsIFile* aDirectory, int64_t aId) +{ + nsCOMPtr<nsIFile> file = GetFileForId(aDirectory, aId); + if (NS_WARN_IF(!file)) { + return nullptr; + } + + DebugOnly<bool> exists; + MOZ_ASSERT(NS_SUCCEEDED(file->Exists(&exists))); + MOZ_ASSERT(exists); + + DebugOnly<bool> isFile; + MOZ_ASSERT(NS_SUCCEEDED(file->IsFile(&isFile))); + MOZ_ASSERT(isFile); + + return file.forget(); +} + +// static +nsresult +FileManager::InitDirectory(nsIFile* aDirectory, + nsIFile* aDatabaseFile, + PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(aDatabaseFile); + + bool exists; + nsresult rv = aDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + return NS_OK; + } + + bool isDirectory; + rv = aDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> journalDirectory; + rv = aDirectory->Clone(getter_AddRefs(journalDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = journalDirectory->Append(NS_LITERAL_STRING(JOURNAL_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = journalDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + rv = journalDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISimpleEnumerator> entries; + rv = journalDirectory->GetDirectoryEntries(getter_AddRefs(entries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasElements; + rv = entries->HasMoreElements(&hasElements); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasElements) { + nsCOMPtr<mozIStorageConnection> connection; + rv = CreateStorageConnection(aDatabaseFile, + aDirectory, + NullString(), + aPersistenceType, + aGroup, + aOrigin, + aTelemetryId, + getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mozStorageTransaction transaction(connection, false); + + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE VIRTUAL TABLE fs USING filesystem;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageStatement> stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT name, (name IN (SELECT id FROM file)) " + "FROM fs " + "WHERE path = :path" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString path; + rv = journalDirectory->GetPath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + nsString name; + rv = stmt->GetString(0, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t flag = stmt->AsInt32(1); + + if (!flag) { + nsCOMPtr<nsIFile> file; + rv = aDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->Append(name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_FAILED(file->Remove(false))) { + NS_WARNING("Failed to remove orphaned file!"); + } + } + + nsCOMPtr<nsIFile> journalFile; + rv = journalDirectory->Clone(getter_AddRefs(journalFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = journalFile->Append(name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_FAILED(journalFile->Remove(false))) { + NS_WARNING("Failed to remove journal file!"); + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE fs;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = transaction.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + return NS_OK; +} + +// static +nsresult +FileManager::GetUsage(nsIFile* aDirectory, uint64_t* aUsage) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(aUsage); + + bool exists; + nsresult rv = aDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + *aUsage = 0; + return NS_OK; + } + + nsCOMPtr<nsISimpleEnumerator> entries; + rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint64_t usage = 0; + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && hasMore) { + nsCOMPtr<nsISupports> entry; + rv = entries->GetNext(getter_AddRefs(entry)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> file = do_QueryInterface(entry); + MOZ_ASSERT(file); + + nsString leafName; + rv = file->GetLeafName(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (leafName.EqualsLiteral(JOURNAL_DIRECTORY_NAME)) { + continue; + } + + int64_t fileSize; + rv = file->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + UsageInfo::IncrementUsage(&usage, uint64_t(fileSize)); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aUsage = usage; + return NS_OK; +} + +/******************************************************************************* + * FileImplStoredFile + ******************************************************************************/ + +NS_IMPL_ISUPPORTS_INHERITED(BlobImplStoredFile, + BlobImplFile, + BlobImplStoredFile) + +/******************************************************************************* + * QuotaClient + ******************************************************************************/ + +QuotaClient* QuotaClient::sInstance = nullptr; + +QuotaClient::QuotaClient() + : mShutdownRequested(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!sInstance, "We expect this to be a singleton!"); + MOZ_ASSERT(!gTelemetryIdMutex); + + // Always create this so that later access to gTelemetryIdHashtable can be + // properly synchronized. + gTelemetryIdMutex = new Mutex("IndexedDB gTelemetryIdMutex"); + + sInstance = this; +} + +QuotaClient::~QuotaClient() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sInstance == this, "We expect this to be a singleton!"); + MOZ_ASSERT(gTelemetryIdMutex); + MOZ_ASSERT(!mMaintenanceThreadPool); + + // No one else should be able to touch gTelemetryIdHashtable now that the + // QuotaClient has gone away. + gTelemetryIdHashtable = nullptr; + gTelemetryIdMutex = nullptr; + + sInstance = nullptr; +} + +nsThreadPool* +QuotaClient::GetOrCreateThreadPool() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShutdownRequested); + + if (!mMaintenanceThreadPool) { + RefPtr<nsThreadPool> threadPool = new nsThreadPool(); + + // PR_GetNumberOfProcessors() can return -1 on error, so make sure we + // don't set some huge number here. We add 2 in case some threads block on + // the disk I/O. + const uint32_t threadCount = + std::max(int32_t(PR_GetNumberOfProcessors()), int32_t(1)) + + 2; + + MOZ_ALWAYS_SUCCEEDS(threadPool->SetThreadLimit(threadCount)); + + // Don't keep more than one idle thread. + MOZ_ALWAYS_SUCCEEDS(threadPool->SetIdleThreadLimit(1)); + + // Don't keep idle threads alive very long. + MOZ_ALWAYS_SUCCEEDS(threadPool->SetIdleThreadTimeout(5 * PR_MSEC_PER_SEC)); + + MOZ_ALWAYS_SUCCEEDS(threadPool->SetName(NS_LITERAL_CSTRING("IndexedDB Mnt"))); + + mMaintenanceThreadPool = Move(threadPool); + } + + return mMaintenanceThreadPool; +} + +mozilla::dom::quota::Client::Type +QuotaClient::GetType() +{ + return QuotaClient::IDB; +} + +struct FileManagerInitInfo +{ + nsCOMPtr<nsIFile> mDirectory; + nsCOMPtr<nsIFile> mDatabaseFile; + nsCOMPtr<nsIFile> mDatabaseWALFile; +}; + +nsresult +QuotaClient::InitOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) +{ + AssertIsOnIOThread(); + + nsCOMPtr<nsIFile> directory; + nsresult rv = + GetDirectory(aPersistenceType, aOrigin, getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We need to see if there are any files in the directory already. If they + // are database files then we need to cleanup stored files (if it's needed) + // and also get the usage. + + AutoTArray<nsString, 20> subdirsToProcess; + nsTArray<nsCOMPtr<nsIFile>> unknownFiles; + nsTHashtable<nsStringHashKey> validSubdirs(20); + AutoTArray<FileManagerInitInfo, 20> initInfos; + + nsCOMPtr<nsISimpleEnumerator> entries; + rv = directory->GetDirectoryEntries(getter_AddRefs(entries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + const NS_ConvertASCIItoUTF16 filesSuffix( + kFileManagerDirectoryNameSuffix, + LiteralStringLength(kFileManagerDirectoryNameSuffix)); + + const NS_ConvertASCIItoUTF16 journalSuffix( + kSQLiteJournalSuffix, + LiteralStringLength(kSQLiteJournalSuffix)); + const NS_ConvertASCIItoUTF16 shmSuffix(kSQLiteSHMSuffix, + LiteralStringLength(kSQLiteSHMSuffix)); + const NS_ConvertASCIItoUTF16 walSuffix(kSQLiteWALSuffix, + LiteralStringLength(kSQLiteWALSuffix)); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && + hasMore && + !aCanceled) { + nsCOMPtr<nsISupports> entry; + rv = entries->GetNext(getter_AddRefs(entry)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> file = do_QueryInterface(entry); + MOZ_ASSERT(file); + + nsString leafName; + rv = file->GetLeafName(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDirectory) { + if (!StringEndsWith(leafName, filesSuffix) || + !validSubdirs.GetEntry(leafName)) { + subdirsToProcess.AppendElement(leafName); + } + continue; + } + + // Skip Desktop Service Store (.DS_Store) files. These files are only used + // on Mac OS X, but the profile can be shared across different operating + // systems, so we check it on all platforms. + if (leafName.EqualsLiteral(DSSTORE_FILE_NAME)) { + continue; + } + + // Skip SQLite temporary files. These files take up space on disk but will + // be deleted as soon as the database is opened, so we don't count them + // towards quota. + if (StringEndsWith(leafName, journalSuffix) || + StringEndsWith(leafName, shmSuffix)) { + continue; + } + + // The SQLite WAL file does count towards quota, but it is handled below + // once we find the actual database file. + if (StringEndsWith(leafName, walSuffix)) { + continue; + } + + nsDependentSubstring dbBaseFilename; + if (!GetDatabaseBaseFilename(leafName, dbBaseFilename)) { + unknownFiles.AppendElement(file); + continue; + } + + nsCOMPtr<nsIFile> fmDirectory; + rv = directory->Clone(getter_AddRefs(fmDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString fmDirectoryBaseName = dbBaseFilename + filesSuffix; + + rv = fmDirectory->Append(fmDirectoryBaseName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> walFile; + if (aUsageInfo) { + rv = directory->Clone(getter_AddRefs(walFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = walFile->Append(dbBaseFilename + walSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + FileManagerInitInfo* initInfo = initInfos.AppendElement(); + initInfo->mDirectory.swap(fmDirectory); + initInfo->mDatabaseFile.swap(file); + initInfo->mDatabaseWALFile.swap(walFile); + + validSubdirs.PutEntry(fmDirectoryBaseName); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (uint32_t count = subdirsToProcess.Length(), i = 0; i < count; i++) { + const nsString& subdirName = subdirsToProcess[i]; + + // If the directory has the correct suffix then it must exist in + // validSubdirs. + if (StringEndsWith(subdirName, filesSuffix)) { + if (NS_WARN_IF(!validSubdirs.GetEntry(subdirName))) { + return NS_ERROR_UNEXPECTED; + } + + continue; + } + + // The directory didn't have the right suffix but we might need to rename + // it. Check to see if we have a database that references this directory. + nsString subdirNameWithSuffix = subdirName + filesSuffix; + if (!validSubdirs.GetEntry(subdirNameWithSuffix)) { + // Windows doesn't allow a directory to end with a dot ('.'), so we have + // to check that possibility here too. + // We do this on all platforms, because the origin directory may have + // been created on Windows and now accessed on different OS. + subdirNameWithSuffix = subdirName + NS_LITERAL_STRING(".") + filesSuffix; + if (NS_WARN_IF(!validSubdirs.GetEntry(subdirNameWithSuffix))) { + return NS_ERROR_UNEXPECTED; + } + } + + // We do have a database that uses this directory so we should rename it + // now. However, first check to make sure that we're not overwriting + // something else. + nsCOMPtr<nsIFile> subdir; + rv = directory->Clone(getter_AddRefs(subdir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = subdir->Append(subdirNameWithSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = subdir->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + rv = directory->Clone(getter_AddRefs(subdir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = subdir->Append(subdirName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DebugOnly<bool> isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(subdir->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory); + + rv = subdir->RenameTo(nullptr, subdirNameWithSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + for (uint32_t count = initInfos.Length(), i = 0; + i < count && !aCanceled; + i++) { + FileManagerInitInfo& initInfo = initInfos[i]; + MOZ_ASSERT(initInfo.mDirectory); + MOZ_ASSERT(initInfo.mDatabaseFile); + MOZ_ASSERT_IF(aUsageInfo, initInfo.mDatabaseWALFile); + + rv = FileManager::InitDirectory(initInfo.mDirectory, + initInfo.mDatabaseFile, + aPersistenceType, + aGroup, + aOrigin, + TelemetryIdForFile(initInfo.mDatabaseFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aUsageInfo) { + int64_t fileSize; + rv = initInfo.mDatabaseFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(fileSize >= 0); + + aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); + + rv = initInfo.mDatabaseWALFile->GetFileSize(&fileSize); + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(fileSize >= 0); + aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); + } else if (NS_WARN_IF(rv != NS_ERROR_FILE_NOT_FOUND && + rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)) { + return rv; + } + + uint64_t usage; + rv = FileManager::GetUsage(initInfo.mDirectory, &usage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aUsageInfo->AppendToFileUsage(usage); + } + } + + // We have to do this after file manager initialization. + if (!unknownFiles.IsEmpty()) { +#ifdef DEBUG + for (uint32_t count = unknownFiles.Length(), i = 0; i < count; i++) { + nsCOMPtr<nsIFile>& unknownFile = unknownFiles[i]; + + nsString leafName; + MOZ_ALWAYS_SUCCEEDS(unknownFile->GetLeafName(leafName)); + + MOZ_ASSERT(!StringEndsWith(leafName, journalSuffix)); + MOZ_ASSERT(!StringEndsWith(leafName, shmSuffix)); + MOZ_ASSERT(!StringEndsWith(leafName, walSuffix)); + + nsString path; + MOZ_ALWAYS_SUCCEEDS(unknownFile->GetPath(path)); + MOZ_ASSERT(!path.IsEmpty()); + + nsPrintfCString warning("Refusing to open databases for \"%s\" because " + "an unexpected file exists in the storage " + "area: \"%s\"", + PromiseFlatCString(aOrigin).get(), + NS_ConvertUTF16toUTF8(path).get()); + NS_WARNING(warning.get()); + } +#endif + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult +QuotaClient::GetUsageForOrigin(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aUsageInfo); + + nsCOMPtr<nsIFile> directory; + nsresult rv = + GetDirectory(aPersistenceType, aOrigin, getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = GetUsageForDirectoryInternal(directory, aCanceled, aUsageInfo, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) +{ + AssertIsOnIOThread(); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->InvalidateFileManagers(aPersistenceType, aOrigin); + } +} + +void +QuotaClient::ReleaseIOThreadObjects() +{ + AssertIsOnIOThread(); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->InvalidateAllFileManagers(); + } +} + +void +QuotaClient::AbortOperations(const nsACString& aOrigin) +{ + AssertIsOnBackgroundThread(); + + if (!gLiveDatabaseHashtable) { + return; + } + + nsTArray<RefPtr<Database>> databases; + + for (auto iter = gLiveDatabaseHashtable->ConstIter(); + !iter.Done(); iter.Next()) { + for (Database* database : iter.Data()->mLiveDatabases) { + if (aOrigin.IsVoid() || database->Origin() == aOrigin) { + databases.AppendElement(database); + } + } + } + + for (Database* database : databases) { + database->Invalidate(); + } + + databases.Clear(); +} + +void +QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) +{ + AssertIsOnBackgroundThread(); + + if (!gLiveDatabaseHashtable) { + return; + } + + nsTArray<RefPtr<Database>> databases; + + for (auto iter = gLiveDatabaseHashtable->ConstIter(); + !iter.Done(); iter.Next()) { + for (Database* database : iter.Data()->mLiveDatabases) { + if (database->IsOwnedByProcess(aContentParentId)) { + databases.AppendElement(database); + } + } + } + + for (Database* database : databases) { + database->Invalidate(); + } + + databases.Clear(); +} + +void +QuotaClient::StartIdleMaintenance() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShutdownRequested); + + mBackgroundThread = do_GetCurrentThread(); + + RefPtr<Maintenance> maintenance = new Maintenance(this); + + mMaintenanceQueue.AppendElement(maintenance.forget()); + ProcessMaintenanceQueue(); +} + +void +QuotaClient::StopIdleMaintenance() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShutdownRequested); + + if (mCurrentMaintenance) { + mCurrentMaintenance->Abort(); + } + + for (RefPtr<Maintenance>& maintenance : mMaintenanceQueue) { + maintenance->Abort(); + } +} + +void +QuotaClient::ShutdownWorkThreads() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShutdownRequested); + + mShutdownRequested = true; + + if (mMaintenanceThreadPool) { + mMaintenanceThreadPool->Shutdown(); + mMaintenanceThreadPool = nullptr; + } + + RefPtr<ConnectionPool> connectionPool = gConnectionPool.get(); + if (connectionPool) { + connectionPool->Shutdown(); + + gConnectionPool = nullptr; + } + + RefPtr<FileHandleThreadPool> fileHandleThreadPool = + gFileHandleThreadPool.get(); + if (fileHandleThreadPool) { + fileHandleThreadPool->Shutdown(); + + gFileHandleThreadPool = nullptr; + } +} + +void +QuotaClient::DidInitialize(QuotaManager* aQuotaManager) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->NoteLiveQuotaManager(aQuotaManager); + } +} + +void +QuotaClient::WillShutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->NoteShuttingDownQuotaManager(); + } +} + +nsresult +QuotaClient::GetDirectory(PersistenceType aPersistenceType, + const nsACString& aOrigin, nsIFile** aDirectory) +{ + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + nsCOMPtr<nsIFile> directory; + nsresult rv = quotaManager->GetDirectoryForOrigin(aPersistenceType, aOrigin, + getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(directory); + + rv = directory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + directory.forget(aDirectory); + return NS_OK; +} + +nsresult +QuotaClient::GetUsageForDirectoryInternal(nsIFile* aDirectory, + const AtomicBool& aCanceled, + UsageInfo* aUsageInfo, + bool aDatabaseFiles) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(aUsageInfo); + + nsCOMPtr<nsISimpleEnumerator> entries; + nsresult rv = aDirectory->GetDirectoryEntries(getter_AddRefs(entries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!entries) { + return NS_OK; + } + + const NS_ConvertASCIItoUTF16 journalSuffix( + kSQLiteJournalSuffix, + LiteralStringLength(kSQLiteJournalSuffix)); + const NS_ConvertASCIItoUTF16 shmSuffix(kSQLiteSHMSuffix, + LiteralStringLength(kSQLiteSHMSuffix)); + + bool hasMore; + while (NS_SUCCEEDED((rv = entries->HasMoreElements(&hasMore))) && + hasMore && + !aCanceled) { + nsCOMPtr<nsISupports> entry; + rv = entries->GetNext(getter_AddRefs(entry)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> file = do_QueryInterface(entry); + MOZ_ASSERT(file); + + nsString leafName; + rv = file->GetLeafName(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Journal files and sqlite-shm files don't count towards usage. + if (StringEndsWith(leafName, journalSuffix) || + StringEndsWith(leafName, shmSuffix)) { + continue; + } + + bool isDirectory; + rv = file->IsDirectory(&isDirectory); + if (rv == NS_ERROR_FILE_NOT_FOUND || + rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { + continue; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDirectory) { + if (aDatabaseFiles) { + rv = GetUsageForDirectoryInternal(file, aCanceled, aUsageInfo, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsString leafName; + rv = file->GetLeafName(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!leafName.EqualsLiteral(JOURNAL_DIRECTORY_NAME)) { + NS_WARNING("Unknown directory found!"); + } + } + + continue; + } + + int64_t fileSize; + rv = file->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(fileSize >= 0); + + if (aDatabaseFiles) { + aUsageInfo->AppendToDatabaseUsage(uint64_t(fileSize)); + } else { + aUsageInfo->AppendToFileUsage(uint64_t(fileSize)); + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +QuotaClient::ProcessMaintenanceQueue() +{ + AssertIsOnBackgroundThread(); + + if (mCurrentMaintenance || mMaintenanceQueue.IsEmpty()) { + return; + } + + mCurrentMaintenance = mMaintenanceQueue[0]; + mMaintenanceQueue.RemoveElementAt(0); + + mCurrentMaintenance->RunImmediately(); +} + +void +Maintenance::RegisterDatabaseMaintenance( + DatabaseMaintenance* aDatabaseMaintenance) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseMaintenance); + MOZ_ASSERT(mState == State::BeginDatabaseMaintenance); + MOZ_ASSERT(!mDatabaseMaintenances.Get(aDatabaseMaintenance->DatabasePath())); + + mDatabaseMaintenances.Put(aDatabaseMaintenance->DatabasePath(), + aDatabaseMaintenance); +} + +void +Maintenance::UnregisterDatabaseMaintenance( + DatabaseMaintenance* aDatabaseMaintenance) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseMaintenance); + MOZ_ASSERT(mState == State::WaitingForDatabaseMaintenancesToComplete); + MOZ_ASSERT(mDatabaseMaintenances.Get(aDatabaseMaintenance->DatabasePath())); + + mDatabaseMaintenances.Remove(aDatabaseMaintenance->DatabasePath()); + + if (mDatabaseMaintenances.Count()) { + return; + } + + mState = State::Finishing; + Finish(); +} + +nsresult +Maintenance::Start() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial); + + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + // Make sure that the IndexedDatabaseManager is running so that we can check + // for low disk space mode. + + if (IndexedDatabaseManager::Get()) { + OpenDirectory(); + return NS_OK; + } + + mState = State::CreateIndexedDatabaseManager; + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return NS_OK; +} + +nsresult +Maintenance::CreateIndexedDatabaseManager() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::CreateIndexedDatabaseManager); + + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetOrCreate(); + if (NS_WARN_IF(!mgr)) { + return NS_ERROR_FAILURE; + } + + mState = State::IndexedDatabaseManagerOpen; + MOZ_ALWAYS_SUCCEEDS( + mQuotaClient->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult +Maintenance::OpenDirectory() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial || + mState == State::IndexedDatabaseManagerOpen); + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(QuotaManager::Get()); + + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + // Get a shared lock for <profile>/storage/*/*/idb + + mState = State::DirectoryOpenPending; + QuotaManager::Get()->OpenDirectoryInternal( + Nullable<PersistenceType>(), + OriginScope::FromNull(), + Nullable<Client::Type>(Client::IDB), + /* aExclusive */ false, + this); + + return NS_OK; +} + +nsresult +Maintenance::DirectoryOpen() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(mDirectoryLock); + + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + mState = State::DirectoryWorkOpen; + + nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +Maintenance::DirectoryWork() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DirectoryWorkOpen); + + // The storage directory is structured like this: + // + // <profile>/storage/<persistence>/<origin>/idb/*.sqlite + // + // We have to find all database files that match any persistence type and any + // origin. We ignore anything out of the ordinary for now. + + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsresult rv = quotaManager->EnsureStorageIsInitialized(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> storageDir = GetFileForPath(quotaManager->GetStoragePath()); + if (NS_WARN_IF(!storageDir)) { + return NS_ERROR_FAILURE; + } + + bool exists; + rv = storageDir->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + return NS_ERROR_NOT_AVAILABLE; + } + + bool isDirectory; + rv = storageDir->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + return NS_ERROR_FAILURE; + } + + // There are currently only 3 persistence types, and we want to iterate them + // in this order: + static const PersistenceType kPersistenceTypes[] = { + PERSISTENCE_TYPE_PERSISTENT, + PERSISTENCE_TYPE_DEFAULT, + PERSISTENCE_TYPE_TEMPORARY + }; + + static_assert((sizeof(kPersistenceTypes) / sizeof(kPersistenceTypes[0])) == + size_t(PERSISTENCE_TYPE_INVALID), + "Something changed with available persistence types!"); + + NS_NAMED_LITERAL_STRING(idbDirName, IDB_DIRECTORY_NAME); + NS_NAMED_LITERAL_STRING(sqliteExtension, ".sqlite"); + + for (const PersistenceType persistenceType : kPersistenceTypes) { + // Loop over "<persistence>" directories. + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + nsAutoCString persistenceTypeString; + if (persistenceType == PERSISTENCE_TYPE_PERSISTENT) { + // XXX This shouldn't be a special case... + persistenceTypeString.AssignLiteral("permanent"); + } else { + PersistenceTypeToText(persistenceType, persistenceTypeString); + } + + nsCOMPtr<nsIFile> persistenceDir; + rv = storageDir->Clone(getter_AddRefs(persistenceDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = persistenceDir->Append(NS_ConvertASCIItoUTF16(persistenceTypeString)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = persistenceDir->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + continue; + } + + rv = persistenceDir->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + continue; + } + + nsCOMPtr<nsISimpleEnumerator> persistenceDirEntries; + rv = persistenceDir->GetDirectoryEntries( + getter_AddRefs(persistenceDirEntries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!persistenceDirEntries) { + continue; + } + + while (true) { + // Loop over "<origin>/idb" directories. + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + bool persistenceDirHasMoreEntries; + rv = persistenceDirEntries->HasMoreElements( + &persistenceDirHasMoreEntries); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!persistenceDirHasMoreEntries) { + break; + } + + nsCOMPtr<nsISupports> persistenceDirEntry; + rv = persistenceDirEntries->GetNext(getter_AddRefs(persistenceDirEntry)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> originDir = do_QueryInterface(persistenceDirEntry); + MOZ_ASSERT(originDir); + + rv = originDir->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(exists); + + rv = originDir->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isDirectory) { + continue; + } + + nsCOMPtr<nsIFile> idbDir; + rv = originDir->Clone(getter_AddRefs(idbDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = idbDir->Append(idbDirName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = idbDir->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + continue; + } + + rv = idbDir->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + continue; + } + + nsCOMPtr<nsISimpleEnumerator> idbDirEntries; + rv = idbDir->GetDirectoryEntries(getter_AddRefs(idbDirEntries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!idbDirEntries) { + continue; + } + + nsCString group; + nsCString origin; + nsTArray<nsString> databasePaths; + + while (true) { + // Loop over files in the "idb" directory. + if (IsAborted()) { + return NS_ERROR_ABORT; + } + + bool idbDirHasMoreEntries; + rv = idbDirEntries->HasMoreElements(&idbDirHasMoreEntries); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!idbDirHasMoreEntries) { + break; + } + + nsCOMPtr<nsISupports> idbDirEntry; + rv = idbDirEntries->GetNext(getter_AddRefs(idbDirEntry)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> idbDirFile = do_QueryInterface(idbDirEntry); + MOZ_ASSERT(idbDirFile); + + nsString idbFilePath; + rv = idbDirFile->GetPath(idbFilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!StringEndsWith(idbFilePath, sqliteExtension)) { + continue; + } + + rv = idbDirFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(exists); + + rv = idbDirFile->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDirectory) { + continue; + } + + // Found a database. + if (databasePaths.IsEmpty()) { + MOZ_ASSERT(group.IsEmpty()); + MOZ_ASSERT(origin.IsEmpty()); + + int64_t dummyTimeStamp; + nsCString dummySuffix; + bool dummyIsApp; + if (NS_WARN_IF(NS_FAILED( + quotaManager->GetDirectoryMetadata2(originDir, + &dummyTimeStamp, + dummySuffix, + group, + origin, + &dummyIsApp)))) { + // Not much we can do here... + continue; + } + } + + MOZ_ASSERT(!databasePaths.Contains(idbFilePath)); + + databasePaths.AppendElement(idbFilePath); + } + + if (!databasePaths.IsEmpty()) { + mDirectoryInfos.AppendElement(DirectoryInfo(persistenceType, + group, + origin, + Move(databasePaths))); + } + } + } + + mState = State::BeginDatabaseMaintenance; + + MOZ_ALWAYS_SUCCEEDS( + mQuotaClient->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult +Maintenance::BeginDatabaseMaintenance() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::BeginDatabaseMaintenance); + + class MOZ_STACK_CLASS Helper final + { + public: + static bool + IsSafeToRunMaintenance(const nsAString& aDatabasePath) + { + if (gFactoryOps) { + for (uint32_t index = gFactoryOps->Length(); index > 0; index--) { + RefPtr<FactoryOp>& existingOp = (*gFactoryOps)[index - 1]; + + MOZ_ASSERT(!existingOp->DatabaseFilePath().IsEmpty()); + + if (existingOp->DatabaseFilePath() == aDatabasePath) { + return false; + } + } + } + + if (gLiveDatabaseHashtable) { + for (auto iter = gLiveDatabaseHashtable->ConstIter(); + !iter.Done(); iter.Next()) { + for (Database* database : iter.Data()->mLiveDatabases) { + if (database->FilePath() == aDatabasePath) { + return false; + } + } + } + } + + return true; + } + }; + + RefPtr<nsThreadPool> threadPool; + + for (DirectoryInfo& directoryInfo : mDirectoryInfos) { + for (const nsString& databasePath : directoryInfo.mDatabasePaths) { + if (Helper::IsSafeToRunMaintenance(databasePath)) { + RefPtr<DatabaseMaintenance> databaseMaintenance = + new DatabaseMaintenance(this, + directoryInfo.mPersistenceType, + directoryInfo.mGroup, + directoryInfo.mOrigin, + databasePath); + + if (!threadPool) { + threadPool = mQuotaClient->GetOrCreateThreadPool(); + MOZ_ASSERT(threadPool); + } + + MOZ_ALWAYS_SUCCEEDS( + threadPool->Dispatch(databaseMaintenance, NS_DISPATCH_NORMAL)); + + RegisterDatabaseMaintenance(databaseMaintenance); + } + } + } + + mDirectoryInfos.Clear(); + + if (mDatabaseMaintenances.Count()) { + mState = State::WaitingForDatabaseMaintenancesToComplete; + } else { + mState = State::Finishing; + Finish(); + } + + return NS_OK; +} + +void +Maintenance::Finish() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Finishing); + + if (NS_FAILED(mResultCode)) { + nsCString errorName; + GetErrorName(mResultCode, errorName); + + IDB_WARNING("Maintenance finished with error: %s", errorName.get()); + } + + mDirectoryLock = nullptr; + + // It can happen that we are only referenced by mCurrentMaintenance which is + // cleared in NoteFinishedMaintenance() + RefPtr<Maintenance> kungFuDeathGrip = this; + + mQuotaClient->NoteFinishedMaintenance(this); + + mState = State::Complete; +} + +NS_IMPL_ISUPPORTS_INHERITED0(Maintenance, Runnable) + +NS_IMETHODIMP +Maintenance::Run() +{ + MOZ_ASSERT(mState != State::Complete); + + nsresult rv; + + switch (mState) { + case State::Initial: + rv = Start(); + break; + + case State::CreateIndexedDatabaseManager: + rv = CreateIndexedDatabaseManager(); + break; + + case State::IndexedDatabaseManagerOpen: + rv = OpenDirectory(); + break; + + case State::DirectoryWorkOpen: + rv = DirectoryWork(); + break; + + case State::BeginDatabaseMaintenance: + rv = BeginDatabaseMaintenance(); + break; + + case State::Finishing: + Finish(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::Finishing) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::Finishing; + + if (IsOnBackgroundThread()) { + Finish(); + } else { + MOZ_ALWAYS_SUCCEEDS( + mQuotaClient->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void +Maintenance::DirectoryLockAcquired(DirectoryLock* aLock) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + nsresult rv = DirectoryOpen(); + if (NS_WARN_IF(NS_FAILED(rv))) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + mState = State::Finishing; + Finish(); + + return; + } +} + +void +Maintenance::DirectoryLockFailed() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + + mState = State::Finishing; + Finish(); +} + +void +DatabaseMaintenance::PerformMaintenanceOnDatabase() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mMaintenance); + MOZ_ASSERT(mMaintenance->StartTime()); + MOZ_ASSERT(!mDatabasePath.IsEmpty()); + MOZ_ASSERT(!mGroup.IsEmpty()); + MOZ_ASSERT(!mOrigin.IsEmpty()); + + class MOZ_STACK_CLASS AutoClose final + { + nsCOMPtr<mozIStorageConnection> mConnection; + + public: + explicit AutoClose(mozIStorageConnection* aConnection) + : mConnection(aConnection) + { + MOZ_ASSERT(aConnection); + } + + ~AutoClose() + { + MOZ_ASSERT(mConnection); + + MOZ_ALWAYS_SUCCEEDS(mConnection->Close()); + } + }; + + nsCOMPtr<nsIFile> databaseFile = GetFileForPath(mDatabasePath); + MOZ_ASSERT(databaseFile); + + nsCOMPtr<mozIStorageConnection> connection; + nsresult rv = GetStorageConnection(databaseFile, + mPersistenceType, + mGroup, + mOrigin, + TelemetryIdForFile(databaseFile), + getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + AutoClose autoClose(connection); + + if (mMaintenance->IsAborted()) { + return; + } + + AutoProgressHandler progressHandler(mMaintenance); + if (NS_WARN_IF(NS_FAILED(progressHandler.Register(connection)))) { + return; + } + + bool databaseIsOk; + rv = CheckIntegrity(connection, &databaseIsOk); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (NS_WARN_IF(!databaseIsOk)) { + // XXX Handle this somehow! Probably need to clear all storage for the + // origin. Needs followup. + MOZ_ASSERT(false, "Database corruption detected!"); + return; + } + + if (mMaintenance->IsAborted()) { + return; + } + + MaintenanceAction maintenanceAction; + rv = DetermineMaintenanceAction(connection, databaseFile, &maintenanceAction); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (mMaintenance->IsAborted()) { + return; + } + + switch (maintenanceAction) { + case MaintenanceAction::Nothing: + break; + + case MaintenanceAction::IncrementalVacuum: + IncrementalVacuum(connection); + break; + + case MaintenanceAction::FullVacuum: + FullVacuum(connection, databaseFile); + break; + + default: + MOZ_CRASH("Unknown MaintenanceAction!"); + } +} + +nsresult +DatabaseMaintenance::CheckIntegrity(mozIStorageConnection* aConnection, + bool* aOk) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aOk); + + nsresult rv; + + // First do a full integrity_check. Scope statements tightly here because + // later operations require zero live statements. + { + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA integrity_check(1);" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + nsString result; + rv = stmt->GetString(0, result); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!result.EqualsLiteral("ok"))) { + *aOk = false; + return NS_OK; + } + } + + // Now enable and check for foreign key constraints. + { + int32_t foreignKeysWereEnabled; + { + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA foreign_keys;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + rv = stmt->GetInt32(0, &foreignKeysWereEnabled); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (!foreignKeysWereEnabled) { + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA foreign_keys = ON;")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool foreignKeyError; + { + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA foreign_key_check;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->ExecuteStep(&foreignKeyError); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (!foreignKeysWereEnabled) { + nsAutoCString stmtSQL; + stmtSQL.AssignLiteral("PRAGMA foreign_keys = "); + stmtSQL.AppendLiteral("OFF"); + stmtSQL.Append(';'); + + rv = aConnection->ExecuteSimpleSQL(stmtSQL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (foreignKeyError) { + *aOk = false; + return NS_OK; + } + } + + *aOk = true; + return NS_OK; +} + +nsresult +DatabaseMaintenance::DetermineMaintenanceAction( + mozIStorageConnection* aConnection, + nsIFile* aDatabaseFile, + MaintenanceAction* aMaintenanceAction) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aDatabaseFile); + MOZ_ASSERT(aMaintenanceAction); + + int32_t schemaVersion; + nsresult rv = aConnection->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Don't do anything if the schema version is less than 18; before that + // version no databases had |auto_vacuum == INCREMENTAL| set and we didn't + // track the values needed for the heuristics below. + if (schemaVersion < MakeSchemaVersion(18, 0)) { + *aMaintenanceAction = MaintenanceAction::Nothing; + return NS_OK; + } + + bool lowDiskSpace = IndexedDatabaseManager::InLowDiskSpaceMode(); + + if (QuotaManager::IsRunningXPCShellTests()) { + // If we're running XPCShell then we want to test both the low disk space + // and normal disk space code paths so pick semi-randomly based on the + // current time. + lowDiskSpace = ((PR_Now() / PR_USEC_PER_MSEC) % 2) == 0; + } + + // If we're low on disk space then the best we can hope for is that an + // incremental vacuum might free some space. That is a journaled operation so + // it may not be possible even then. + if (lowDiskSpace) { + *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; + return NS_OK; + } + + // This method shouldn't make any permanent changes to the database, so make + // sure everything gets rolled back when we leave. + mozStorageTransaction transaction(aConnection, + /* aCommitOnComplete */ false); + + // Check to see when we last vacuumed this database. + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT last_vacuum_time, last_vacuum_size " + "FROM database;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + PRTime lastVacuumTime; + rv = stmt->GetInt64(0, &lastVacuumTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t lastVacuumSize; + rv = stmt->GetInt64(1, &lastVacuumSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_ASSERTION(lastVacuumSize > 0, "Thy last vacuum size shall be greater than zero, less than zero shall thy last vacuum size not be. Zero is right out."); + + PRTime startTime = mMaintenance->StartTime(); + + // This shouldn't really be possible... + if (NS_WARN_IF(startTime <= lastVacuumTime)) { + *aMaintenanceAction = MaintenanceAction::Nothing; + return NS_OK; + } + + if (startTime - lastVacuumTime < kMinVacuumAge) { + *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; + return NS_OK; + } + + // It has been more than a week since the database was vacuumed, so gather + // statistics on its usage to see if vacuuming is worthwhile. + + // Create a temporary copy of the dbstat table to speed up the queries that + // come later. + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE VIRTUAL TABLE __stats__ USING dbstat;" + "CREATE TEMP TABLE __temp_stats__ AS SELECT * FROM __stats__;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Calculate the percentage of the database pages that are not in contiguous + // order. + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT SUM(__ts1__.pageno != __ts2__.pageno + 1) * 100.0 / COUNT(*) " + "FROM __temp_stats__ AS __ts1__, __temp_stats__ AS __ts2__ " + "WHERE __ts1__.name = __ts2__.name " + "AND __ts1__.rowid = __ts2__.rowid + 1;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + int32_t percentUnordered; + rv = stmt->GetInt32(0, &percentUnordered); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(percentUnordered >= 0); + MOZ_ASSERT(percentUnordered <= 100); + + if (percentUnordered >= kPercentUnorderedThreshold) { + *aMaintenanceAction = MaintenanceAction::FullVacuum; + return NS_OK; + } + + // Don't try a full vacuum if the file hasn't grown by 10%. + int64_t currentFileSize; + rv = aDatabaseFile->GetFileSize(¤tFileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (currentFileSize <= lastVacuumSize || + (((currentFileSize - lastVacuumSize) * 100 / currentFileSize) < + kPercentFileSizeGrowthThreshold)) { + *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; + return NS_OK; + } + + // See if there are any free pages that we can reclaim. + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA freelist_count;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + int32_t freelistCount; + rv = stmt->GetInt32(0, &freelistCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(freelistCount >= 0); + + // If we have too many free pages then we should try an incremental vacuum. If + // that causes too much fragmentation then we'll try a full vacuum later. + if (freelistCount > kMaxFreelistThreshold) { + *aMaintenanceAction = MaintenanceAction::IncrementalVacuum; + return NS_OK; + } + + // Calculate the percentage of unused bytes on pages in the database. + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT SUM(unused) * 100.0 / SUM(pgsize) " + "FROM __temp_stats__;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(hasResult); + + int32_t percentUnused; + rv = stmt->GetInt32(0, &percentUnused); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(percentUnused >= 0); + MOZ_ASSERT(percentUnused <= 100); + + *aMaintenanceAction = percentUnused >= kPercentUnusedThreshold ? + MaintenanceAction::FullVacuum : + MaintenanceAction::IncrementalVacuum; + return NS_OK; +} + +void +DatabaseMaintenance::IncrementalVacuum(mozIStorageConnection* aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA incremental_vacuum;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void +DatabaseMaintenance::FullVacuum(mozIStorageConnection* aConnection, + nsIFile* aDatabaseFile) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aDatabaseFile); + + nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "VACUUM;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime > 0); + + int64_t fileSize; + rv = aDatabaseFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT(fileSize > 0); + + nsCOMPtr<mozIStorageStatement> stmt; + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("time"), vacuumTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("size"), fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void +DatabaseMaintenance::RunOnOwningThread() +{ + AssertIsOnBackgroundThread(); + + if (mCompleteCallback) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget())); + } + + mMaintenance->UnregisterDatabaseMaintenance(this); +} + +void +DatabaseMaintenance::RunOnConnectionThread() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + PerformMaintenanceOnDatabase(); + + MOZ_ALWAYS_SUCCEEDS( + mMaintenance->BackgroundThread()->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +NS_IMETHODIMP +DatabaseMaintenance::Run() +{ + if (IsOnBackgroundThread()) { + RunOnOwningThread(); + } else { + RunOnConnectionThread(); + } + + return NS_OK; +} + +nsresult +DatabaseMaintenance:: +AutoProgressHandler::Register(mozIStorageConnection* aConnection) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + + // We want to quickly bail out of any operation if the user becomes active, so + // use a small granularity here since database performance isn't critical. + static const int32_t kProgressGranularity = 50; + + nsCOMPtr<mozIStorageProgressHandler> oldHandler; + nsresult rv = aConnection->SetProgressHandler(kProgressGranularity, + this, + getter_AddRefs(oldHandler)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!oldHandler); + mConnection = aConnection; + + return NS_OK; +} + +void +DatabaseMaintenance:: +AutoProgressHandler::Unregister() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mConnection); + + nsCOMPtr<mozIStorageProgressHandler> oldHandler; + nsresult rv = mConnection->RemoveProgressHandler(getter_AddRefs(oldHandler)); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + MOZ_ASSERT_IF(NS_SUCCEEDED(rv), oldHandler == this); +} + +NS_IMETHODIMP_(MozExternalRefCountType) +DatabaseMaintenance:: +AutoProgressHandler::AddRef() +{ + NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); + +#ifdef DEBUG + mDEBUGRefCnt++; +#endif + return 2; +} + +NS_IMETHODIMP_(MozExternalRefCountType) +DatabaseMaintenance:: +AutoProgressHandler::Release() +{ + NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); + +#ifdef DEBUG + mDEBUGRefCnt--; +#endif + return 1; +} + +NS_IMPL_QUERY_INTERFACE(DatabaseMaintenance::AutoProgressHandler, + mozIStorageProgressHandler) + +NS_IMETHODIMP +DatabaseMaintenance:: +AutoProgressHandler::OnProgress(mozIStorageConnection* aConnection, + bool* _retval) +{ + NS_ASSERT_OWNINGTHREAD(DatabaseMaintenance::AutoProgressHandler); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(mConnection == aConnection); + MOZ_ASSERT(_retval); + + *_retval = mMaintenance->IsAborted(); + + return NS_OK; +} + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(CompressDataBlobsFunction, mozIStorageFunction) +NS_IMPL_ISUPPORTS(EncodeKeysFunction, mozIStorageFunction) + +#if !defined(MOZ_B2G) + +nsresult +UpgradeFileIdsFunction::Init(nsIFile* aFMDirectory, + mozIStorageConnection* aConnection) +{ + // This file manager doesn't need real origin info, etc. The only purpose is + // to store file ids without adding more complexity or code duplication. + RefPtr<FileManager> fileManager = + new FileManager(PERSISTENCE_TYPE_INVALID, + EmptyCString(), + EmptyCString(), + false, + EmptyString(), + false); + + nsresult rv = fileManager->Init(aFMDirectory, aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoPtr<NormalJSContext> context(NormalJSContext::Create()); + if (NS_WARN_IF(!context)) { + return NS_ERROR_FAILURE; + } + + mFileManager.swap(fileManager); + mContext = context; + return NS_OK; +} + +NS_IMPL_ISUPPORTS(UpgradeFileIdsFunction, mozIStorageFunction) + +NS_IMETHODIMP +UpgradeFileIdsFunction::OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) +{ + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(mContext); + + PROFILER_LABEL("IndexedDB", + "UpgradeFileIdsFunction::OnFunctionCall", + js::ProfileEntry::Category::STORAGE); + + uint32_t argc; + nsresult rv = aArguments->GetNumEntries(&argc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (argc != 2) { + NS_WARNING("Don't call me with the wrong number of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + StructuredCloneReadInfo cloneInfo; + DatabaseOperationBase::GetStructuredCloneReadInfoFromValueArray(aArguments, + 1, + 0, + mFileManager, + &cloneInfo); + + JSContext* cx = mContext->Context(); + JSAutoRequest ar(cx); + JSAutoCompartment ac(cx, mContext->Global()); + + JS::Rooted<JS::Value> clone(cx); + if (NS_WARN_IF(!IDBObjectStore::DeserializeUpgradeValue(cx, cloneInfo, + &clone))) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + nsAutoString fileIds; + + for (uint32_t count = cloneInfo.mFiles.Length(), index = 0; + index < count; + index++) { + StructuredCloneFile& file = cloneInfo.mFiles[index]; + MOZ_ASSERT(file.mFileInfo); + + const int64_t id = file.mFileInfo->Id(); + + if (index) { + fileIds.Append(' '); + } + fileIds.AppendInt(file.mType == StructuredCloneFile::eBlob ? id : -id); + } + + nsCOMPtr<nsIVariant> result = new mozilla::storage::TextVariant(fileIds); + + result.forget(aResult); + return NS_OK; +} + +#endif // MOZ_B2G + +// static +void +DatabaseOperationBase::GetBindingClauseForKeyRange( + const SerializedKeyRange& aKeyRange, + const nsACString& aKeyColumnName, + nsAutoCString& aBindingClause) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!aKeyColumnName.IsEmpty()); + + NS_NAMED_LITERAL_CSTRING(andStr, " AND "); + NS_NAMED_LITERAL_CSTRING(spacecolon, " :"); + NS_NAMED_LITERAL_CSTRING(lowerKey, "lower_key"); + + if (aKeyRange.isOnly()) { + // Both keys equal. + aBindingClause = andStr + aKeyColumnName + NS_LITERAL_CSTRING(" =") + + spacecolon + lowerKey; + return; + } + + aBindingClause.Truncate(); + + if (!aKeyRange.lower().IsUnset()) { + // Lower key is set. + aBindingClause.Append(andStr + aKeyColumnName); + aBindingClause.AppendLiteral(" >"); + if (!aKeyRange.lowerOpen()) { + aBindingClause.AppendLiteral("="); + } + aBindingClause.Append(spacecolon + lowerKey); + } + + if (!aKeyRange.upper().IsUnset()) { + // Upper key is set. + aBindingClause.Append(andStr + aKeyColumnName); + aBindingClause.AppendLiteral(" <"); + if (!aKeyRange.upperOpen()) { + aBindingClause.AppendLiteral("="); + } + aBindingClause.Append(spacecolon + NS_LITERAL_CSTRING("upper_key")); + } + + MOZ_ASSERT(!aBindingClause.IsEmpty()); +} + +// static +uint64_t +DatabaseOperationBase::ReinterpretDoubleAsUInt64(double aDouble) +{ + // This is a duplicate of the js engine's byte munging in StructuredClone.cpp + return BitwiseCast<uint64_t>(aDouble); +} + +// static +template <typename T> +nsresult +DatabaseOperationBase::GetStructuredCloneReadInfoFromSource( + T* aSource, + uint32_t aDataIndex, + uint32_t aFileIdsIndex, + FileManager* aFileManager, + StructuredCloneReadInfo* aInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aSource); + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aInfo); + + int32_t columnType; + nsresult rv = aSource->GetTypeOfIndex(aDataIndex, &columnType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_BLOB || + columnType == mozIStorageStatement::VALUE_TYPE_INTEGER); + + bool isNull; + rv = aSource->GetIsNull(aFileIdsIndex, &isNull); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString fileIds; + + if (isNull) { + fileIds.SetIsVoid(true); + } else { + rv = aSource->GetString(aFileIdsIndex, fileIds); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (columnType == mozIStorageStatement::VALUE_TYPE_INTEGER) { + uint64_t intData; + rv = aSource->GetInt64(aDataIndex, reinterpret_cast<int64_t*>(&intData)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = GetStructuredCloneReadInfoFromExternalBlob(intData, + aFileManager, + fileIds, + aInfo); + } else { + const uint8_t* blobData; + uint32_t blobDataLength; + nsresult rv = + aSource->GetSharedBlob(aDataIndex, &blobDataLength, &blobData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = GetStructuredCloneReadInfoFromBlob(blobData, + blobDataLength, + aFileManager, + fileIds, + aInfo); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::GetStructuredCloneReadInfoFromBlob( + const uint8_t* aBlobData, + uint32_t aBlobDataLength, + FileManager* aFileManager, + const nsAString& aFileIds, + StructuredCloneReadInfo* aInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aInfo); + + PROFILER_LABEL("IndexedDB", + "DatabaseOperationBase::GetStructuredCloneReadInfoFromBlob", + js::ProfileEntry::Category::STORAGE); + + const char* compressed = reinterpret_cast<const char*>(aBlobData); + size_t compressedLength = size_t(aBlobDataLength); + + size_t uncompressedLength; + if (NS_WARN_IF(!snappy::GetUncompressedLength(compressed, compressedLength, + &uncompressedLength))) { + return NS_ERROR_FILE_CORRUPTED; + } + + AutoTArray<uint8_t, 512> uncompressed; + if (NS_WARN_IF(!uncompressed.SetLength(uncompressedLength, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* uncompressedBuffer = reinterpret_cast<char*>(uncompressed.Elements()); + + if (NS_WARN_IF(!snappy::RawUncompress(compressed, compressedLength, + uncompressedBuffer))) { + return NS_ERROR_FILE_CORRUPTED; + } + + if (!aInfo->mData.WriteBytes(uncompressedBuffer, uncompressed.Length())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!aFileIds.IsVoid()) { + nsresult rv = DeserializeStructuredCloneFiles(aFileManager, + aFileIds, + aInfo->mFiles, + &aInfo->mHasPreprocessInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::GetStructuredCloneReadInfoFromExternalBlob( + uint64_t aIntData, + FileManager* aFileManager, + const nsAString& aFileIds, + StructuredCloneReadInfo* aInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aInfo); + + PROFILER_LABEL( + "IndexedDB", + "DatabaseOperationBase::GetStructuredCloneReadInfoFromExternalBlob", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + + if (!aFileIds.IsVoid()) { + rv = DeserializeStructuredCloneFiles(aFileManager, + aFileIds, + aInfo->mFiles, + &aInfo->mHasPreprocessInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Higher and lower 32 bits described + // in ObjectStoreAddOrPutRequestOp::DoDatabaseWork. + uint32_t index = uint32_t(aIntData & 0xFFFFFFFF); + + if (index >= aInfo->mFiles.Length()) { + MOZ_ASSERT(false, "Bad index value!"); + return NS_ERROR_UNEXPECTED; + } + + StructuredCloneFile& file = aInfo->mFiles[index]; + MOZ_ASSERT(file.mFileInfo); + MOZ_ASSERT(file.mType == StructuredCloneFile::eStructuredClone); + + nsCOMPtr<nsIFile> nativeFile = GetFileForFileInfo(file.mFileInfo); + if (NS_WARN_IF(!nativeFile)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIInputStream> fileInputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), nativeFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<SnappyUncompressInputStream> snappyInputStream = + new SnappyUncompressInputStream(fileInputStream); + + do { + char buffer[kFileCopyBufferSize]; + + uint32_t numRead; + rv = snappyInputStream->Read(buffer, sizeof(buffer), &numRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + if (!numRead) { + break; + } + + if (NS_WARN_IF(!aInfo->mData.WriteBytes(buffer, numRead))) { + rv = NS_ERROR_OUT_OF_MEMORY; + break; + } + } while (true); + + return rv; +} + +// static +nsresult +DatabaseOperationBase::BindKeyRangeToStatement( + const SerializedKeyRange& aKeyRange, + mozIStorageStatement* aStatement) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aStatement); + + nsresult rv = NS_OK; + + if (!aKeyRange.lower().IsUnset()) { + rv = aKeyRange.lower().BindToStatement(aStatement, NS_LITERAL_CSTRING("lower_key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (aKeyRange.isOnly()) { + return rv; + } + + if (!aKeyRange.upper().IsUnset()) { + rv = aKeyRange.upper().BindToStatement(aStatement, NS_LITERAL_CSTRING("upper_key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::BindKeyRangeToStatement( + const SerializedKeyRange& aKeyRange, + mozIStorageStatement* aStatement, + const nsCString& aLocale) +{ +#ifndef ENABLE_INTL_API + return BindKeyRangeToStatement(aKeyRange, aStatement); +#else + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aStatement); + MOZ_ASSERT(!aLocale.IsEmpty()); + + nsresult rv = NS_OK; + + if (!aKeyRange.lower().IsUnset()) { + Key lower; + rv = aKeyRange.lower().ToLocaleBasedKey(lower, aLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = lower.BindToStatement(aStatement, NS_LITERAL_CSTRING("lower_key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (aKeyRange.isOnly()) { + return rv; + } + + if (!aKeyRange.upper().IsUnset()) { + Key upper; + rv = aKeyRange.upper().ToLocaleBasedKey(upper, aLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = upper.BindToStatement(aStatement, NS_LITERAL_CSTRING("upper_key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +#endif +} + +// static +void +DatabaseOperationBase::AppendConditionClause(const nsACString& aColumnName, + const nsACString& aArgName, + bool aLessThan, + bool aEquals, + nsAutoCString& aResult) +{ + aResult += NS_LITERAL_CSTRING(" AND ") + aColumnName + + NS_LITERAL_CSTRING(" "); + + if (aLessThan) { + aResult.Append('<'); + } + else { + aResult.Append('>'); + } + + if (aEquals) { + aResult.Append('='); + } + + aResult += NS_LITERAL_CSTRING(" :") + aArgName; +} + +// static +nsresult +DatabaseOperationBase::GetUniqueIndexTableForObjectStore( + TransactionBase* aTransaction, + int64_t aObjectStoreId, + Maybe<UniqueIndexTable>& aMaybeUniqueIndexTable) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(aMaybeUniqueIndexTable.isNothing()); + + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + aTransaction->GetMetadataForObjectStoreId(aObjectStoreId); + MOZ_ASSERT(objectStoreMetadata); + + if (!objectStoreMetadata->mIndexes.Count()) { + return NS_OK; + } + + const uint32_t indexCount = objectStoreMetadata->mIndexes.Count(); + MOZ_ASSERT(indexCount > 0); + + aMaybeUniqueIndexTable.emplace(); + UniqueIndexTable* uniqueIndexTable = aMaybeUniqueIndexTable.ptr(); + MOZ_ASSERT(uniqueIndexTable); + + for (auto iter = objectStoreMetadata->mIndexes.Iter(); !iter.Done(); iter.Next()) { + FullIndexMetadata* value = iter.UserData(); + MOZ_ASSERT(!uniqueIndexTable->Get(value->mCommonMetadata.id())); + + if (NS_WARN_IF(!uniqueIndexTable->Put(value->mCommonMetadata.id(), + value->mCommonMetadata.unique(), + fallible))) { + break; + } + } + + if (NS_WARN_IF(aMaybeUniqueIndexTable.ref().Count() != indexCount)) { + IDB_REPORT_INTERNAL_ERR(); + aMaybeUniqueIndexTable.reset(); + NS_WARNING("out of memory"); + return NS_ERROR_OUT_OF_MEMORY; + } + +#ifdef DEBUG + aMaybeUniqueIndexTable.ref().MarkImmutable(); +#endif + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::IndexDataValuesFromUpdateInfos( + const nsTArray<IndexUpdateInfo>& aUpdateInfos, + const UniqueIndexTable& aUniqueIndexTable, + nsTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(aIndexValues.IsEmpty()); + MOZ_ASSERT_IF(!aUpdateInfos.IsEmpty(), aUniqueIndexTable.Count()); + + PROFILER_LABEL("IndexedDB", + "DatabaseOperationBase::IndexDataValuesFromUpdateInfos", + js::ProfileEntry::Category::STORAGE); + + const uint32_t count = aUpdateInfos.Length(); + + if (!count) { + return NS_OK; + } + + if (NS_WARN_IF(!aIndexValues.SetCapacity(count, fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + for (uint32_t idxIndex = 0; idxIndex < count; idxIndex++) { + const IndexUpdateInfo& updateInfo = aUpdateInfos[idxIndex]; + const int64_t& indexId = updateInfo.indexId(); + const Key& key = updateInfo.value(); + const Key& sortKey = updateInfo.localizedValue(); + + bool unique = false; + MOZ_ALWAYS_TRUE(aUniqueIndexTable.Get(indexId, &unique)); + + IndexDataValue idv(indexId, unique, key, sortKey); + + MOZ_ALWAYS_TRUE( + aIndexValues.InsertElementSorted(idv, fallible)); + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::InsertIndexTableRows( + DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const Key& aObjectStoreKey, + const FallibleTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + + PROFILER_LABEL("IndexedDB", + "DatabaseOperationBase::InsertIndexTableRows", + js::ProfileEntry::Category::STORAGE); + + const uint32_t count = aIndexValues.Length(); + if (!count) { + return NS_OK; + } + + NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); + NS_NAMED_LITERAL_CSTRING(objectDataKeyString, "object_data_key"); + NS_NAMED_LITERAL_CSTRING(indexIdString, "index_id"); + NS_NAMED_LITERAL_CSTRING(valueString, "value"); + NS_NAMED_LITERAL_CSTRING(valueLocaleString, "value_locale"); + + DatabaseConnection::CachedStatement insertUniqueStmt; + DatabaseConnection::CachedStatement insertStmt; + + nsresult rv; + + for (uint32_t index = 0; index < count; index++) { + const IndexDataValue& info = aIndexValues[index]; + + DatabaseConnection::CachedStatement& stmt = + info.mUnique ? insertUniqueStmt : insertStmt; + + if (stmt) { + stmt.Reset(); + } else if (info.mUnique) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT INTO unique_index_data " + "(index_id, value, object_store_id, object_data_key, value_locale) " + "VALUES (:index_id, :value, :object_store_id, :object_data_key, :value_locale);"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT OR IGNORE INTO index_data " + "(index_id, value, object_data_key, object_store_id, value_locale) " + "VALUES (:index_id, :value, :object_data_key, :object_store_id, :value_locale);"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = stmt->BindInt64ByName(indexIdString, info.mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = info.mKey.BindToStatement(stmt, valueString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = info.mSortKey.BindToStatement(stmt, valueLocaleString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(objectStoreIdString, aObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aObjectStoreKey.BindToStatement(stmt, objectDataKeyString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (rv == NS_ERROR_STORAGE_CONSTRAINT && info.mUnique) { + // If we're inserting multiple entries for the same unique index, then + // we might have failed to insert due to colliding with another entry for + // the same index in which case we should ignore it. + for (int32_t index2 = int32_t(index) - 1; + index2 >= 0 && aIndexValues[index2].mIndexId == info.mIndexId; + --index2) { + if (info.mKey == aIndexValues[index2].mKey) { + // We found a key with the same value for the same index. So we + // must have had a collision with a value we just inserted. + rv = NS_OK; + break; + } + } + } + + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::DeleteIndexDataTableRows( + DatabaseConnection* aConnection, + const Key& aObjectStoreKey, + const FallibleTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + + PROFILER_LABEL("IndexedDB", + "DatabaseOperationBase::DeleteIndexDataTableRows", + js::ProfileEntry::Category::STORAGE); + + const uint32_t count = aIndexValues.Length(); + if (!count) { + return NS_OK; + } + + NS_NAMED_LITERAL_CSTRING(indexIdString, "index_id"); + NS_NAMED_LITERAL_CSTRING(valueString, "value"); + NS_NAMED_LITERAL_CSTRING(objectDataKeyString, "object_data_key"); + + DatabaseConnection::CachedStatement deleteUniqueStmt; + DatabaseConnection::CachedStatement deleteStmt; + + nsresult rv; + + for (uint32_t index = 0; index < count; index++) { + const IndexDataValue& indexValue = aIndexValues[index]; + + DatabaseConnection::CachedStatement& stmt = + indexValue.mUnique ? deleteUniqueStmt : deleteStmt; + + if (stmt) { + stmt.Reset(); + } else if (indexValue.mUnique) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM unique_index_data " + "WHERE index_id = :index_id " + "AND value = :value;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM index_data " + "WHERE index_id = :index_id " + "AND value = :value " + "AND object_data_key = :object_data_key;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = stmt->BindInt64ByName(indexIdString, indexValue.mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = indexValue.mKey.BindToStatement(stmt, valueString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!indexValue.mUnique) { + rv = aObjectStoreKey.BindToStatement(stmt, objectDataKeyString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::DeleteObjectStoreDataTableRowsWithIndexes( + DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const OptionalKeyRange& aKeyRange) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aObjectStoreId); + +#ifdef DEBUG + { + bool hasIndexes = false; + MOZ_ASSERT(NS_SUCCEEDED( + ObjectStoreHasIndexes(aConnection, aObjectStoreId, &hasIndexes))); + MOZ_ASSERT(hasIndexes, + "Don't use this slow method if there are no indexes!"); + } +#endif + + PROFILER_LABEL("IndexedDB", + "DatabaseOperationBase::" + "DeleteObjectStoreDataTableRowsWithIndexes", + js::ProfileEntry::Category::STORAGE); + + const bool singleRowOnly = + aKeyRange.type() == OptionalKeyRange::TSerializedKeyRange && + aKeyRange.get_SerializedKeyRange().isOnly(); + + NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); + NS_NAMED_LITERAL_CSTRING(keyString, "key"); + + nsresult rv; + Key objectStoreKey; + DatabaseConnection::CachedStatement selectStmt; + + if (singleRowOnly) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT index_data_values " + "FROM object_data " + "WHERE object_store_id = :object_store_id " + "AND key = :key;"), + &selectStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + objectStoreKey = aKeyRange.get_SerializedKeyRange().lower(); + + rv = objectStoreKey.BindToStatement(selectStmt, keyString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsAutoCString keyRangeClause; + if (aKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) { + GetBindingClauseForKeyRange(aKeyRange.get_SerializedKeyRange(), + keyString, + keyRangeClause); + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT index_data_values, key " + "FROM object_data " + "WHERE object_store_id = :") + + objectStoreIdString + + keyRangeClause + + NS_LITERAL_CSTRING(";"), + &selectStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) { + rv = BindKeyRangeToStatement(aKeyRange, selectStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + rv = selectStmt->BindInt64ByName(objectStoreIdString, aObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement deleteStmt; + AutoTArray<IndexDataValue, 32> indexValues; + + DebugOnly<uint32_t> resultCountDEBUG = 0; + + bool hasResult; + while (NS_SUCCEEDED(rv = selectStmt->ExecuteStep(&hasResult)) && hasResult) { + if (!singleRowOnly) { + rv = objectStoreKey.SetFromStatement(selectStmt, 1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + indexValues.ClearAndRetainStorage(); + } + + rv = ReadCompressedIndexDataValues(selectStmt, 0, indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = DeleteIndexDataTableRows(aConnection, objectStoreKey, indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (deleteStmt) { + MOZ_ALWAYS_SUCCEEDS(deleteStmt->Reset()); + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_data " + "WHERE object_store_id = :object_store_id " + "AND key = :key;"), + &deleteStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = deleteStmt->BindInt64ByName(objectStoreIdString, aObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = objectStoreKey.BindToStatement(deleteStmt, keyString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = deleteStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + resultCountDEBUG++; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT_IF(singleRowOnly, resultCountDEBUG <= 1); + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::UpdateIndexValues( + DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const Key& aObjectStoreKey, + const FallibleTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + + PROFILER_LABEL("IndexedDB", + "DatabaunseOperationBase::UpdateIndexValues", + js::ProfileEntry::Category::STORAGE); + + UniqueFreePtr<uint8_t> indexDataValues; + uint32_t indexDataValuesLength; + nsresult rv = MakeCompressedIndexDataValues(aIndexValues, + indexDataValues, + &indexDataValuesLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!indexDataValuesLength == !(indexDataValues.get())); + + DatabaseConnection::CachedStatement updateStmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE object_data " + "SET index_data_values = :index_data_values " + "WHERE object_store_id = :object_store_id " + "AND key = :key;"), + &updateStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_NAMED_LITERAL_CSTRING(indexDataValuesString, "index_data_values"); + + if (indexDataValues) { + rv = updateStmt->BindAdoptedBlobByName(indexDataValuesString, + indexDataValues.release(), + indexDataValuesLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = updateStmt->BindNullByName(indexDataValuesString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + aObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aObjectStoreKey.BindToStatement(updateStmt, NS_LITERAL_CSTRING("key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = updateStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +nsresult +DatabaseOperationBase::ObjectStoreHasIndexes(DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + bool* aHasIndexes) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(aHasIndexes); + + DatabaseConnection::CachedStatement stmt; + + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT id " + "FROM object_store_index " + "WHERE object_store_id = :object_store_id " + "LIMIT 1;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + aObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aHasIndexes = hasResult; + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED(DatabaseOperationBase, + Runnable, + mozIStorageProgressHandler) + +NS_IMETHODIMP +DatabaseOperationBase::OnProgress(mozIStorageConnection* aConnection, + bool* _retval) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(_retval); + + // This is intentionally racy. + *_retval = !OperationMayProceed(); + return NS_OK; +} + +DatabaseOperationBase:: +AutoSetProgressHandler::AutoSetProgressHandler() + : mConnection(nullptr) +#ifdef DEBUG + , mDEBUGDatabaseOp(nullptr) +#endif +{ + MOZ_ASSERT(!IsOnBackgroundThread()); +} + +DatabaseOperationBase:: +AutoSetProgressHandler::~AutoSetProgressHandler() +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + + if (mConnection) { + nsCOMPtr<mozIStorageProgressHandler> oldHandler; + MOZ_ALWAYS_SUCCEEDS( + mConnection->RemoveProgressHandler(getter_AddRefs(oldHandler))); + MOZ_ASSERT(oldHandler == mDEBUGDatabaseOp); + } +} + +nsresult +DatabaseOperationBase:: +AutoSetProgressHandler::Register(mozIStorageConnection* aConnection, + DatabaseOperationBase* aDatabaseOp) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(aDatabaseOp); + MOZ_ASSERT(!mConnection); + + nsCOMPtr<mozIStorageProgressHandler> oldProgressHandler; + + nsresult rv = + aConnection->SetProgressHandler(kStorageProgressGranularity, + aDatabaseOp, + getter_AddRefs(oldProgressHandler)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!oldProgressHandler); + + mConnection = aConnection; +#ifdef DEBUG + mDEBUGDatabaseOp = aDatabaseOp; +#endif + + return NS_OK; +} + +MutableFile::MutableFile(nsIFile* aFile, + Database* aDatabase, + FileInfo* aFileInfo) + : BackgroundMutableFileParentBase(FILE_HANDLE_STORAGE_IDB, + aDatabase->Id(), + IntString(aFileInfo->Id()), + aFile) + , mDatabase(aDatabase) + , mFileInfo(aFileInfo) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(aFileInfo); +} + +MutableFile::~MutableFile() +{ + mDatabase->UnregisterMutableFile(this); +} + +already_AddRefed<MutableFile> +MutableFile::Create(nsIFile* aFile, + Database* aDatabase, + FileInfo* aFileInfo) +{ + AssertIsOnBackgroundThread(); + + RefPtr<MutableFile> newMutableFile = + new MutableFile(aFile, aDatabase, aFileInfo); + + if (!aDatabase->RegisterMutableFile(newMutableFile)) { + return nullptr; + } + + return newMutableFile.forget(); +} + +void +MutableFile::NoteActiveState() +{ + AssertIsOnBackgroundThread(); + + mDatabase->NoteActiveMutableFile(); +} + +void +MutableFile::NoteInactiveState() +{ + AssertIsOnBackgroundThread(); + + mDatabase->NoteInactiveMutableFile(); +} + +PBackgroundParent* +MutableFile::GetBackgroundParent() const +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return GetDatabase()->GetBackgroundParent(); +} + +already_AddRefed<nsISupports> +MutableFile::CreateStream(bool aReadOnly) +{ + AssertIsOnBackgroundThread(); + + PersistenceType persistenceType = mDatabase->Type(); + const nsACString& group = mDatabase->Group(); + const nsACString& origin = mDatabase->Origin(); + + nsCOMPtr<nsISupports> result; + + if (aReadOnly) { + RefPtr<FileInputStream> stream = + FileInputStream::Create(persistenceType, group, origin, mFile, -1, -1, + nsIFileInputStream::DEFER_OPEN); + result = NS_ISUPPORTS_CAST(nsIFileInputStream*, stream); + } + else { + RefPtr<FileStream> stream = + FileStream::Create(persistenceType, group, origin, mFile, -1, -1, + nsIFileStream::DEFER_OPEN); + result = NS_ISUPPORTS_CAST(nsIFileStream*, stream); + } + if (NS_WARN_IF(!result)) { + return nullptr; + } + + return result.forget(); +} + +already_AddRefed<BlobImpl> +MutableFile::CreateBlobImpl() +{ + AssertIsOnBackgroundThread(); + + RefPtr<BlobImpl> blobImpl = + new BlobImplStoredFile(mFile, mFileInfo, /* aSnapshot */ true); + return blobImpl.forget(); +} + +PBackgroundFileHandleParent* +MutableFile::AllocPBackgroundFileHandleParent(const FileMode& aMode) +{ + AssertIsOnBackgroundThread(); + + // Once a database is closed it must not try to open new file handles. + if (NS_WARN_IF(mDatabase->IsClosed())) { + if (!mDatabase->IsInvalidated()) { + ASSERT_UNLESS_FUZZING(); + } + return nullptr; + } + + if (!gFileHandleThreadPool) { + RefPtr<FileHandleThreadPool> fileHandleThreadPool = + FileHandleThreadPool::Create(); + if (NS_WARN_IF(!fileHandleThreadPool)) { + return nullptr; + } + + gFileHandleThreadPool = fileHandleThreadPool; + } + + return BackgroundMutableFileParentBase::AllocPBackgroundFileHandleParent( + aMode); +} + +bool +MutableFile::RecvPBackgroundFileHandleConstructor( + PBackgroundFileHandleParent* aActor, + const FileMode& aMode) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mDatabase->IsClosed()); + + if (NS_WARN_IF(mDatabase->IsInvalidated())) { + // This is an expected race. We don't want the child to die here, just don't + // actually do any work. + return true; + } + + return BackgroundMutableFileParentBase::RecvPBackgroundFileHandleConstructor( + aActor, aMode); +} + +bool +MutableFile::RecvGetFileId(int64_t* aFileId) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFileInfo); + + if (NS_WARN_IF(!IndexedDatabaseManager::InTestingMode())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + *aFileId = mFileInfo->Id(); + return true; +} + +FactoryOp::FactoryOp(Factory* aFactory, + already_AddRefed<ContentParent> aContentParent, + const CommonFactoryRequestParams& aCommonParams, + bool aDeleting) + : DatabaseOperationBase(aFactory->GetLoggingInfo()->Id(), + aFactory->GetLoggingInfo()->NextRequestSN()) + , mFactory(aFactory) + , mContentParent(Move(aContentParent)) + , mCommonParams(aCommonParams) + , mState(State::Initial) + , mIsApp(false) + , mEnforcingQuota(true) + , mDeleting(aDeleting) + , mBlockedDatabaseOpen(false) + , mChromeWriteAccessAllowed(false) + , mFileHandleDisabled(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFactory); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); +} + +nsresult +FactoryOp::Open() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::Initial); + + // Swap this to the stack now to ensure that we release it on this thread. + RefPtr<ContentParent> contentParent; + mContentParent.swap(contentParent); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + PermissionRequestBase::PermissionValue permission; + nsresult rv = CheckPermission(contentParent, &permission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed || + permission == PermissionRequestBase::kPermissionDenied || + permission == PermissionRequestBase::kPermissionPrompt); + + if (permission == PermissionRequestBase::kPermissionDenied) { + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + { + // These services have to be started on the main thread currently. + + IndexedDatabaseManager* mgr; + if (NS_WARN_IF(!(mgr = IndexedDatabaseManager::GetOrCreate()))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsCOMPtr<mozIStorageService> ss; + if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + const DatabaseMetadata& metadata = mCommonParams.metadata(); + + QuotaManager::GetStorageId(metadata.persistenceType(), + mOrigin, + Client::IDB, + mDatabaseId); + + mDatabaseId.Append('*'); + mDatabaseId.Append(NS_ConvertUTF16toUTF8(metadata.name())); + + if (permission == PermissionRequestBase::kPermissionPrompt) { + mState = State::PermissionChallenge; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + return NS_OK; + } + + MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed); + + mState = State::FinishOpen; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult +FactoryOp::ChallengePermission() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::PermissionChallenge); + + const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + if (NS_WARN_IF(!SendPermissionChallenge(principalInfo))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +FactoryOp::RetryCheckPermission() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::PermissionRetry); + MOZ_ASSERT(mCommonParams.principalInfo().type() == + PrincipalInfo::TContentPrincipalInfo); + + // Swap this to the stack now to ensure that we release it on this thread. + RefPtr<ContentParent> contentParent; + mContentParent.swap(contentParent); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + PermissionRequestBase::PermissionValue permission; + nsresult rv = CheckPermission(contentParent, &permission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed || + permission == PermissionRequestBase::kPermissionDenied || + permission == PermissionRequestBase::kPermissionPrompt); + + if (permission == PermissionRequestBase::kPermissionDenied || + permission == PermissionRequestBase::kPermissionPrompt) { + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + MOZ_ASSERT(permission == PermissionRequestBase::kPermissionAllowed); + + mState = State::FinishOpen; + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult +FactoryOp::DirectoryOpen() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + + // gFactoryOps could be null here if the child process crashed or something + // and that cleaned up the last Factory actor. + if (!gFactoryOps) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // See if this FactoryOp needs to wait. + bool delayed = false; + for (uint32_t index = gFactoryOps->Length(); index > 0; index--) { + RefPtr<FactoryOp>& existingOp = (*gFactoryOps)[index - 1]; + if (MustWaitFor(*existingOp)) { + // Only one op can be delayed. + MOZ_ASSERT(!existingOp->mDelayedOp); + existingOp->mDelayedOp = this; + delayed = true; + break; + } + } + + // Adding this to the factory ops list will block any additional ops from + // proceeding until this one is done. + gFactoryOps->AppendElement(this); + + if (!delayed) { + QuotaClient* quotaClient = QuotaClient::GetInstance(); + MOZ_ASSERT(quotaClient); + + if (RefPtr<Maintenance> currentMaintenance = + quotaClient->GetCurrentMaintenance()) { + if (RefPtr<DatabaseMaintenance> databaseMaintenance = + currentMaintenance->GetDatabaseMaintenance(mDatabaseFilePath)) { + databaseMaintenance->WaitForCompletion(this); + delayed = true; + } + } + } + + mBlockedDatabaseOpen = true; + + // Balanced in FinishSendResults(). + IncreaseBusyCount(); + + mState = State::DatabaseOpenPending; + if (!delayed) { + nsresult rv = DatabaseOpen(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +nsresult +FactoryOp::SendToIOThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseOpenPending); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO thread. + mState = State::DatabaseWorkOpen; + + nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +void +FactoryOp::WaitForTransactions() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::BeginVersionChange || + mState == State::WaitingForOtherDatabasesToClose); + MOZ_ASSERT(!mDatabaseId.IsEmpty()); + MOZ_ASSERT(!IsActorDestroyed()); + + mState = State::WaitingForTransactionsToComplete; + + RefPtr<WaitForTransactionsHelper> helper = + new WaitForTransactionsHelper(mDatabaseId, this); + helper->WaitForTransactions(); +} + +void +FactoryOp::FinishSendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(mFactory); + + // Make sure to release the factory on this thread. + RefPtr<Factory> factory; + mFactory.swap(factory); + + if (mBlockedDatabaseOpen) { + if (mDelayedOp) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); + } + + MOZ_ASSERT(gFactoryOps); + gFactoryOps->RemoveElement(this); + + // Match the IncreaseBusyCount in DirectoryOpen(). + DecreaseBusyCount(); + } + + mState = State::Completed; +} + +nsresult +FactoryOp::CheckPermission(ContentParent* aContentParent, + PermissionRequestBase::PermissionValue* aPermission) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::Initial || mState == State::PermissionRetry); + + const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); + if (principalInfo.type() != PrincipalInfo::TSystemPrincipalInfo) { + if (principalInfo.type() != PrincipalInfo::TContentPrincipalInfo) { + if (aContentParent) { + // We just want ContentPrincipalInfo or SystemPrincipalInfo. + aContentParent->KillHard("IndexedDB CheckPermission 0"); + } + + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + if (NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) { + if (aContentParent) { + // The DOM in the other process should have kept us from receiving any + // indexedDB messages so assume that the child is misbehaving. + aContentParent->KillHard("IndexedDB CheckPermission 1"); + } + + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + const ContentPrincipalInfo& contentPrincipalInfo = + principalInfo.get_ContentPrincipalInfo(); + if (contentPrincipalInfo.attrs().mPrivateBrowsingId != 0) { + // IndexedDB is currently disabled in privateBrowsing. + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + } + + mFileHandleDisabled = !Preferences::GetBool(kPrefFileHandleEnabled); + + PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); + + MOZ_ASSERT(principalInfo.type() != PrincipalInfo::TNullPrincipalInfo); + + if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + MOZ_ASSERT(mState == State::Initial); + MOZ_ASSERT(persistenceType == PERSISTENCE_TYPE_PERSISTENT); + + if (aContentParent) { + // Check to make sure that the child process has access to the database it + // is accessing. + NS_NAMED_LITERAL_CSTRING(permissionStringBase, + PERMISSION_STRING_CHROME_BASE); + NS_ConvertUTF16toUTF8 databaseName(mCommonParams.metadata().name()); + NS_NAMED_LITERAL_CSTRING(readSuffix, PERMISSION_STRING_CHROME_READ_SUFFIX); + NS_NAMED_LITERAL_CSTRING(writeSuffix, PERMISSION_STRING_CHROME_WRITE_SUFFIX); + + const nsAutoCString permissionStringWrite = + permissionStringBase + databaseName + writeSuffix; + const nsAutoCString permissionStringRead = + permissionStringBase + databaseName + readSuffix; + + bool canWrite = + CheckAtLeastOneAppHasPermission(aContentParent, permissionStringWrite); + + bool canRead; + if (canWrite) { + MOZ_ASSERT(CheckAtLeastOneAppHasPermission(aContentParent, + permissionStringRead)); + canRead = true; + } else { + canRead = + CheckAtLeastOneAppHasPermission(aContentParent, permissionStringRead); + } + + // Deleting a database requires write permissions. + if (mDeleting && !canWrite) { + aContentParent->KillHard("IndexedDB CheckPermission 2"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // Opening or deleting requires read permissions. + if (!canRead) { + aContentParent->KillHard("IndexedDB CheckPermission 3"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mChromeWriteAccessAllowed = canWrite; + } else { + mChromeWriteAccessAllowed = true; + } + + if (State::Initial == mState) { + QuotaManager::GetInfoForChrome(&mSuffix, &mGroup, &mOrigin, &mIsApp); + + MOZ_ASSERT(!QuotaManager::IsFirstPromptRequired(persistenceType, mOrigin, + mIsApp)); + + mEnforcingQuota = + QuotaManager::IsQuotaEnforced(persistenceType, mOrigin, mIsApp); + } + + *aPermission = PermissionRequestBase::kPermissionAllowed; + return NS_OK; + } + + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + nsresult rv; + nsCOMPtr<nsIPrincipal> principal = + PrincipalInfoToPrincipal(principalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString suffix; + nsCString group; + nsCString origin; + bool isApp; + rv = QuotaManager::GetInfoFromPrincipal(principal, + &suffix, + &group, + &origin, + &isApp); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef IDB_MOBILE + if (persistenceType == PERSISTENCE_TYPE_PERSISTENT && + !QuotaManager::IsOriginInternal(origin) && + !isApp) { + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } +#endif + + PermissionRequestBase::PermissionValue permission; + + if (QuotaManager::IsFirstPromptRequired(persistenceType, origin, isApp)) { + rv = PermissionRequestBase::GetCurrentPermission(principal, &permission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + permission = PermissionRequestBase::kPermissionAllowed; + } + + if (permission != PermissionRequestBase::kPermissionDenied && + State::Initial == mState) { + mSuffix = suffix; + mGroup = group; + mOrigin = origin; + mIsApp = isApp; + + mEnforcingQuota = + QuotaManager::IsQuotaEnforced(persistenceType, mOrigin, mIsApp); + } + + *aPermission = permission; + return NS_OK; +} + +nsresult +FactoryOp::SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo, + Database* aOpeningDatabase, + uint64_t aOldVersion, + const NullableVersion& aNewVersion) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabaseActorInfo); + MOZ_ASSERT(mState == State::BeginVersionChange); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(!IsActorDestroyed()); + + const uint32_t expectedCount = mDeleting ? 0 : 1; + const uint32_t liveCount = aDatabaseActorInfo->mLiveDatabases.Length(); + if (liveCount > expectedCount) { + FallibleTArray<MaybeBlockedDatabaseInfo> maybeBlockedDatabases; + for (uint32_t index = 0; index < liveCount; index++) { + Database* database = aDatabaseActorInfo->mLiveDatabases[index]; + if ((!aOpeningDatabase || database != aOpeningDatabase) && + !database->IsClosed() && + NS_WARN_IF(!maybeBlockedDatabases.AppendElement(database, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + if (!maybeBlockedDatabases.IsEmpty()) { + mMaybeBlockedDatabases.SwapElements(maybeBlockedDatabases); + } + } + + if (!mMaybeBlockedDatabases.IsEmpty()) { + for (uint32_t count = mMaybeBlockedDatabases.Length(), index = 0; + index < count; + /* incremented conditionally */) { + if (mMaybeBlockedDatabases[index]->SendVersionChange(aOldVersion, + aNewVersion)) { + index++; + } else { + // We don't want to wait forever if we were not able to send the + // message. + mMaybeBlockedDatabases.RemoveElementAt(index); + count--; + } + } + } + + return NS_OK; +} + +// static +bool +FactoryOp::CheckAtLeastOneAppHasPermission(ContentParent* aContentParent, + const nsACString& aPermissionString) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aContentParent); + MOZ_ASSERT(!aPermissionString.IsEmpty()); + + return true; +} + +nsresult +FactoryOp::FinishOpen() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::FinishOpen); + MOZ_ASSERT(!mContentParent); + + if (QuotaManager::Get()) { + nsresult rv = OpenDirectory(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + mState = State::QuotaManagerPending; + QuotaManager::GetOrCreate(this); + + return NS_OK; +} + +nsresult +FactoryOp::QuotaManagerOpen() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::QuotaManagerPending); + + if (NS_WARN_IF(!QuotaManager::Get())) { + return NS_ERROR_FAILURE; + } + + nsresult rv = OpenDirectory(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +FactoryOp::OpenDirectory() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::FinishOpen || + mState == State::QuotaManagerPending); + MOZ_ASSERT(!mOrigin.IsEmpty()); + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(QuotaManager::Get()); + + // Need to get database file path in advance. + const nsString& databaseName = mCommonParams.metadata().name(); + PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsCOMPtr<nsIFile> dbFile; + nsresult rv = quotaManager->GetDirectoryForOrigin(persistenceType, + mOrigin, + getter_AddRefs(dbFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = dbFile->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoString filename; + GetDatabaseFilename(databaseName, filename); + + rv = dbFile->Append(filename + NS_LITERAL_STRING(".sqlite")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = dbFile->GetPath(mDatabaseFilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mState = State::DirectoryOpenPending; + + quotaManager->OpenDirectory(persistenceType, + mGroup, + mOrigin, + mIsApp, + Client::IDB, + /* aExclusive */ false, + this); + + return NS_OK; +} + +bool +FactoryOp::MustWaitFor(const FactoryOp& aExistingOp) +{ + AssertIsOnOwningThread(); + + // Things for the same persistence type, the same origin and the same + // database must wait. + return aExistingOp.mCommonParams.metadata().persistenceType() == + mCommonParams.metadata().persistenceType() && + aExistingOp.mOrigin == mOrigin && + aExistingOp.mDatabaseId == mDatabaseId; +} + +void +FactoryOp::NoteDatabaseBlocked(Database* aDatabase) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); + MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(mMaybeBlockedDatabases.Contains(aDatabase)); + + // Only send the blocked event if all databases have reported back. If the + // database was closed then it will have been removed from the array. + // Otherwise if it was blocked its |mBlocked| flag will be true. + bool sendBlockedEvent = true; + + for (uint32_t count = mMaybeBlockedDatabases.Length(), index = 0; + index < count; + index++) { + MaybeBlockedDatabaseInfo& info = mMaybeBlockedDatabases[index]; + if (info == aDatabase) { + // This database was blocked, mark accordingly. + info.mBlocked = true; + } else if (!info.mBlocked) { + // A database has not yet reported back yet, don't send the event yet. + sendBlockedEvent = false; + } + } + + if (sendBlockedEvent) { + SendBlockedNotification(); + } +} + +NS_IMPL_ISUPPORTS_INHERITED0(FactoryOp, DatabaseOperationBase) + +// Run() assumes that the caller holds a strong reference to the object that +// can't be cleared while Run() is being executed. +// So if you call Run() directly (as opposed to dispatching to an event queue) +// you need to make sure there's such a reference. +// See bug 1356824 for more details. +NS_IMETHODIMP +FactoryOp::Run() +{ + nsresult rv; + + switch (mState) { + case State::Initial: + rv = Open(); + break; + + case State::PermissionChallenge: + rv = ChallengePermission(); + break; + + case State::PermissionRetry: + rv = RetryCheckPermission(); + break; + + case State::FinishOpen: + rv = FinishOpen(); + break; + + case State::QuotaManagerPending: + rv = QuotaManagerOpen(); + break; + + case State::DatabaseOpenPending: + rv = DatabaseOpen(); + break; + + case State::DatabaseWorkOpen: + rv = DoDatabaseWork(); + break; + + case State::BeginVersionChange: + rv = BeginVersionChange(); + break; + + case State::WaitingForTransactionsToComplete: + rv = DispatchToWorkThread(); + break; + + case State::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingResults; + + if (IsOnOwningThread()) { + SendResults(); + } else { + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +void +FactoryOp::DirectoryLockAcquired(DirectoryLock* aLock) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + nsresult rv = DirectoryOpen(); + if (NS_WARN_IF(NS_FAILED(rv))) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + // The caller holds a strong reference to us, no need for a self reference + // before calling Run(). + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(Run()); + + return; + } +} + +void +FactoryOp::DirectoryLockFailed() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + if (NS_SUCCEEDED(mResultCode)) { + IDB_REPORT_INTERNAL_ERR(); + mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // The caller holds a strong reference to us, no need for a self reference + // before calling Run(). + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(Run()); +} + +void +FactoryOp::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + + NoteActorDestroyed(); +} + +bool +FactoryOp::RecvPermissionRetry() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!IsActorDestroyed()); + MOZ_ASSERT(mState == State::PermissionChallenge); + + mContentParent = BackgroundParent::GetContentParent(Manager()->Manager()); + + mState = State::PermissionRetry; + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return true; +} + +OpenDatabaseOp::OpenDatabaseOp(Factory* aFactory, + already_AddRefed<ContentParent> aContentParent, + const CommonFactoryRequestParams& aParams) + : FactoryOp(aFactory, Move(aContentParent), aParams, /* aDeleting */ false) + , mMetadata(new FullDatabaseMetadata(aParams.metadata())) + , mRequestedVersion(aParams.metadata().version()) + , mVersionChangeOp(nullptr) + , mTelemetryId(0) +{ + if (mContentParent) { + // This is a little scary but it looks safe to call this off the main thread + // for now. + mOptionalContentParentId = Some(mContentParent->ChildID()); + } +} + +void +OpenDatabaseOp::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + FactoryOp::ActorDestroy(aWhy); + + if (mVersionChangeOp) { + mVersionChangeOp->NoteActorDestroyed(); + } +} + +nsresult +OpenDatabaseOp::DatabaseOpen() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseOpenPending); + + nsresult rv = SendToIOThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +OpenDatabaseOp::DoDatabaseWork() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + + PROFILER_LABEL("IndexedDB", + "OpenDatabaseOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const nsString& databaseName = mCommonParams.metadata().name(); + PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsCOMPtr<nsIFile> dbDirectory; + + nsresult rv = + quotaManager->EnsureOriginIsInitialized(persistenceType, + mSuffix, + mGroup, + mOrigin, + mIsApp, + getter_AddRefs(dbDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = dbDirectory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = dbDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + rv = dbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } +#ifdef DEBUG + else { + bool isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(dbDirectory->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory); + } +#endif + + nsAutoString filename; + GetDatabaseFilename(databaseName, filename); + + nsCOMPtr<nsIFile> dbFile; + rv = dbDirectory->Clone(getter_AddRefs(dbFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = dbFile->Append(filename + NS_LITERAL_STRING(".sqlite")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mTelemetryId = TelemetryIdForFile(dbFile); + +#ifdef DEBUG + nsString databaseFilePath; + rv = dbFile->GetPath(databaseFilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); +#endif + + nsCOMPtr<nsIFile> fmDirectory; + rv = dbDirectory->Clone(getter_AddRefs(fmDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + const NS_ConvertASCIItoUTF16 filesSuffix(kFileManagerDirectoryNameSuffix); + + rv = fmDirectory->Append(filename + filesSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageConnection> connection; + rv = CreateStorageConnection(dbFile, + fmDirectory, + databaseName, + persistenceType, + mGroup, + mOrigin, + mTelemetryId, + getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AutoSetProgressHandler asph; + rv = asph.Register(connection, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = LoadDatabaseInformation(connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(mMetadata->mNextObjectStoreId > mMetadata->mObjectStores.Count()); + MOZ_ASSERT(mMetadata->mNextIndexId > 0); + + // See if we need to do a versionchange transaction + + // Optional version semantics. + if (!mRequestedVersion) { + // If the requested version was not specified and the database was created, + // treat it as if version 1 were requested. + if (mMetadata->mCommonMetadata.version() == 0) { + mRequestedVersion = 1; + } else { + // Otherwise, treat it as if the current version were requested. + mRequestedVersion = mMetadata->mCommonMetadata.version(); + } + } + + if (NS_WARN_IF(mMetadata->mCommonMetadata.version() > mRequestedVersion)) { + return NS_ERROR_DOM_INDEXEDDB_VERSION_ERR; + } + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); + MOZ_ASSERT(mgr); + + RefPtr<FileManager> fileManager = + mgr->GetFileManager(persistenceType, mOrigin, databaseName); + if (!fileManager) { + fileManager = new FileManager(persistenceType, + mGroup, + mOrigin, + mIsApp, + databaseName, + mEnforcingQuota); + + rv = fileManager->Init(fmDirectory, connection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mgr->AddFileManager(fileManager); + } + + mFileManager = fileManager.forget(); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = (mMetadata->mCommonMetadata.version() == mRequestedVersion) ? + State::SendingResults : + State::BeginVersionChange; + + rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +OpenDatabaseOp::LoadDatabaseInformation(mozIStorageConnection* aConnection) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(mMetadata); + + // Load version information. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT name, origin, version " + "FROM database" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasResult)) { + return NS_ERROR_FILE_CORRUPTED; + } + + nsString databaseName; + rv = stmt->GetString(0, databaseName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(mCommonParams.metadata().name() != databaseName)) { + return NS_ERROR_FILE_CORRUPTED; + } + + nsCString origin; + rv = stmt->GetUTF8String(1, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mOrigin != origin) { + NS_WARNING("Origins don't match!"); + } + + int64_t version; + rv = stmt->GetInt64(2, &version); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mMetadata->mCommonMetadata.version() = uint64_t(version); + + ObjectStoreTable& objectStores = mMetadata->mObjectStores; + + // Load object store names and ids. + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, auto_increment, name, key_path " + "FROM object_store" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Maybe<nsTHashtable<nsUint64HashKey>> usedIds; + Maybe<nsTHashtable<nsStringHashKey>> usedNames; + + int64_t lastObjectStoreId = 0; + + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + int64_t objectStoreId; + rv = stmt->GetInt64(0, &objectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!usedIds) { + usedIds.emplace(); + } + + if (NS_WARN_IF(objectStoreId <= 0) || + NS_WARN_IF(usedIds.ref().Contains(objectStoreId))) { + return NS_ERROR_FILE_CORRUPTED; + } + + if (NS_WARN_IF(!usedIds.ref().PutEntry(objectStoreId, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsString name; + rv = stmt->GetString(2, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!usedNames) { + usedNames.emplace(); + } + + if (NS_WARN_IF(usedNames.ref().Contains(name))) { + return NS_ERROR_FILE_CORRUPTED; + } + + if (NS_WARN_IF(!usedNames.ref().PutEntry(name, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr<FullObjectStoreMetadata> metadata = new FullObjectStoreMetadata(); + metadata->mCommonMetadata.id() = objectStoreId; + metadata->mCommonMetadata.name() = name; + + int32_t columnType; + rv = stmt->GetTypeOfIndex(3, &columnType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) { + metadata->mCommonMetadata.keyPath() = KeyPath(0); + } else { + MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_TEXT); + + nsString keyPathSerialization; + rv = stmt->GetString(3, keyPathSerialization); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + metadata->mCommonMetadata.keyPath() = + KeyPath::DeserializeFromString(keyPathSerialization); + if (NS_WARN_IF(!metadata->mCommonMetadata.keyPath().IsValid())) { + return NS_ERROR_FILE_CORRUPTED; + } + } + + int64_t nextAutoIncrementId; + rv = stmt->GetInt64(1, &nextAutoIncrementId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + metadata->mCommonMetadata.autoIncrement() = !!nextAutoIncrementId; + metadata->mNextAutoIncrementId = nextAutoIncrementId; + metadata->mCommittedAutoIncrementId = nextAutoIncrementId; + + if (NS_WARN_IF(!objectStores.Put(objectStoreId, metadata, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + lastObjectStoreId = std::max(lastObjectStoreId, objectStoreId); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + usedIds.reset(); + usedNames.reset(); + + // Load index information + rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "id, object_store_id, name, key_path, unique_index, multientry, " + "locale, is_auto_locale " + "FROM object_store_index" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t lastIndexId = 0; + + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + int64_t objectStoreId; + rv = stmt->GetInt64(1, &objectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + RefPtr<FullObjectStoreMetadata> objectStoreMetadata; + if (NS_WARN_IF(!objectStores.Get(objectStoreId, + getter_AddRefs(objectStoreMetadata)))) { + return NS_ERROR_FILE_CORRUPTED; + } + + MOZ_ASSERT(objectStoreMetadata->mCommonMetadata.id() == objectStoreId); + + int64_t indexId; + rv = stmt->GetInt64(0, &indexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!usedIds) { + usedIds.emplace(); + } + + if (NS_WARN_IF(indexId <= 0) || + NS_WARN_IF(usedIds.ref().Contains(indexId))) { + return NS_ERROR_FILE_CORRUPTED; + } + + if (NS_WARN_IF(!usedIds.ref().PutEntry(indexId, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsString name; + rv = stmt->GetString(2, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoString hashName; + hashName.AppendInt(indexId); + hashName.Append(':'); + hashName.Append(name); + + if (!usedNames) { + usedNames.emplace(); + } + + if (NS_WARN_IF(usedNames.ref().Contains(hashName))) { + return NS_ERROR_FILE_CORRUPTED; + } + + if (NS_WARN_IF(!usedNames.ref().PutEntry(hashName, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr<FullIndexMetadata> indexMetadata = new FullIndexMetadata(); + indexMetadata->mCommonMetadata.id() = indexId; + indexMetadata->mCommonMetadata.name() = name; + +#ifdef DEBUG + { + int32_t columnType; + rv = stmt->GetTypeOfIndex(3, &columnType); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(columnType != mozIStorageStatement::VALUE_TYPE_NULL); + } +#endif + + nsString keyPathSerialization; + rv = stmt->GetString(3, keyPathSerialization); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + indexMetadata->mCommonMetadata.keyPath() = + KeyPath::DeserializeFromString(keyPathSerialization); + if (NS_WARN_IF(!indexMetadata->mCommonMetadata.keyPath().IsValid())) { + return NS_ERROR_FILE_CORRUPTED; + } + + int32_t scratch; + rv = stmt->GetInt32(4, &scratch); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + indexMetadata->mCommonMetadata.unique() = !!scratch; + + rv = stmt->GetInt32(5, &scratch); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + indexMetadata->mCommonMetadata.multiEntry() = !!scratch; + +#ifdef ENABLE_INTL_API + const bool localeAware = !stmt->IsNull(6); + if (localeAware) { + rv = stmt->GetUTF8String(6, indexMetadata->mCommonMetadata.locale()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->GetInt32(7, &scratch); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + indexMetadata->mCommonMetadata.autoLocale() = !!scratch; + + // Update locale-aware indexes if necessary + const nsCString& indexedLocale = indexMetadata->mCommonMetadata.locale(); + const bool& isAutoLocale = indexMetadata->mCommonMetadata.autoLocale(); + nsCString systemLocale = IndexedDatabaseManager::GetLocale(); + if (!systemLocale.IsEmpty() && + isAutoLocale && + !indexedLocale.EqualsASCII(systemLocale.get())) { + rv = UpdateLocaleAwareIndex(aConnection, + indexMetadata->mCommonMetadata, + systemLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } +#endif + + if (NS_WARN_IF(!objectStoreMetadata->mIndexes.Put(indexId, indexMetadata, + fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + lastIndexId = std::max(lastIndexId, indexId); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(lastObjectStoreId == INT64_MAX) || + NS_WARN_IF(lastIndexId == INT64_MAX)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mMetadata->mNextObjectStoreId = lastObjectStoreId + 1; + mMetadata->mNextIndexId = lastIndexId + 1; + + return NS_OK; +} + +#ifdef ENABLE_INTL_API +/* static */ +nsresult +OpenDatabaseOp::UpdateLocaleAwareIndex(mozIStorageConnection* aConnection, + const IndexMetadata& aIndexMetadata, + const nsCString& aLocale) +{ + nsresult rv; + + nsCString indexTable; + if (aIndexMetadata.unique()) { + indexTable.AssignLiteral("unique_index_data"); + } + else { + indexTable.AssignLiteral("index_data"); + } + + nsCString readQuery = NS_LITERAL_CSTRING("SELECT value, object_data_key FROM ") + + indexTable + + NS_LITERAL_CSTRING(" WHERE index_id = :index_id"); + nsCOMPtr<mozIStorageStatement> readStmt; + rv = aConnection->CreateStatement(readQuery, getter_AddRefs(readStmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = readStmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), + aIndexMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageStatement> writeStmt; + bool needCreateWriteQuery = true; + bool hasResult; + while (NS_SUCCEEDED((rv = readStmt->ExecuteStep(&hasResult))) && hasResult) { + if (needCreateWriteQuery) { + needCreateWriteQuery = false; + nsCString writeQuery = NS_LITERAL_CSTRING("UPDATE ") + indexTable + + NS_LITERAL_CSTRING("SET value_locale = :value_locale " + "WHERE index_id = :index_id AND " + "value = :value AND " + "object_data_key = :object_data_key"); + rv = aConnection->CreateStatement(writeQuery, getter_AddRefs(writeStmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mozStorageStatementScoper scoper(writeStmt); + rv = writeStmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), + aIndexMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + Key oldKey, newSortKey, objectKey; + rv = oldKey.SetFromStatement(readStmt, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = oldKey.BindToStatement(writeStmt, NS_LITERAL_CSTRING("value")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = oldKey.ToLocaleBasedKey(newSortKey, aLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = newSortKey.BindToStatement(writeStmt, + NS_LITERAL_CSTRING("value_locale")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = objectKey.SetFromStatement(readStmt, 1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = objectKey.BindToStatement(writeStmt, + NS_LITERAL_CSTRING("object_data_key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = writeStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsCString metaQuery = NS_LITERAL_CSTRING("UPDATE object_store_index SET " + "locale = :locale WHERE id = :id"); + nsCOMPtr<mozIStorageStatement> metaStmt; + rv = aConnection->CreateStatement(metaQuery, getter_AddRefs(metaStmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString locale; + locale.AssignWithConversion(aLocale); + rv = metaStmt->BindStringByName(NS_LITERAL_CSTRING("locale"), locale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = metaStmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aIndexMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = metaStmt->Execute(); + return rv; +} +#endif + +nsresult +OpenDatabaseOp::BeginVersionChange() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::BeginVersionChange); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(mMetadata->mCommonMetadata.version() <= mRequestedVersion); + MOZ_ASSERT(!mDatabase); + MOZ_ASSERT(!mVersionChangeTransaction); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + EnsureDatabaseActor(); + + if (mDatabase->IsInvalidated()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + MOZ_ASSERT(!mDatabase->IsClosed()); + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); + + MOZ_ASSERT(info->mLiveDatabases.Contains(mDatabase)); + MOZ_ASSERT(!info->mWaitingFactoryOp); + MOZ_ASSERT(info->mMetadata == mMetadata); + + RefPtr<VersionChangeTransaction> transaction = + new VersionChangeTransaction(this); + + if (NS_WARN_IF(!transaction->CopyDatabaseMetadata())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(info->mMetadata != mMetadata); + mMetadata = info->mMetadata; + + NullableVersion newVersion = mRequestedVersion; + + nsresult rv = + SendVersionChangeMessages(info, + mDatabase, + mMetadata->mCommonMetadata.version(), + newVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mVersionChangeTransaction.swap(transaction); + + if (mMaybeBlockedDatabases.IsEmpty()) { + // We don't need to wait on any databases, just jump to the transaction + // pool. + WaitForTransactions(); + return NS_OK; + } + + info->mWaitingFactoryOp = this; + + mState = State::WaitingForOtherDatabasesToClose; + return NS_OK; +} + +void +OpenDatabaseOp::NoteDatabaseClosed(Database* aDatabase) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose || + mState == State::WaitingForTransactionsToComplete || + mState == State::DatabaseWorkVersionChange); + + if (mState != State::WaitingForOtherDatabasesToClose) { + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(mRequestedVersion > + aDatabase->Metadata()->mCommonMetadata.version(), + "Must only be closing databases for a previous version!"); + return; + } + + MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); + + bool actorDestroyed = IsActorDestroyed() || mDatabase->IsActorDestroyed(); + + nsresult rv; + if (actorDestroyed) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } else { + rv = NS_OK; + } + + // We are being called with an assuption that mWaitingFactoryOp holds a strong + // reference to us. + RefPtr<OpenDatabaseOp> kungFuDeathGrip; + + if (mMaybeBlockedDatabases.RemoveElement(aDatabase) && + mMaybeBlockedDatabases.IsEmpty()) { + if (actorDestroyed) { + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); + MOZ_ASSERT(info->mWaitingFactoryOp == this); + kungFuDeathGrip = + static_cast<OpenDatabaseOp*>(info->mWaitingFactoryOp.get()); + info->mWaitingFactoryOp = nullptr; + } else { + WaitForTransactions(); + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + // A strong reference is held in kungFuDeathGrip, so it's safe to call Run() + // directly. + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(Run()); + } +} + +void +OpenDatabaseOp::SendBlockedNotification() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); + + if (!IsActorDestroyed()) { + Unused << SendBlocked(mMetadata->mCommonMetadata.version()); + } +} + +nsresult +OpenDatabaseOp::DispatchToWorkThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForTransactionsToComplete); + MOZ_ASSERT(mVersionChangeTransaction); + MOZ_ASSERT(mVersionChangeTransaction->GetMode() == + IDBTransaction::VERSION_CHANGE); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed() || + mDatabase->IsInvalidated()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mState = State::DatabaseWorkVersionChange; + + // Intentionally empty. + nsTArray<nsString> objectStoreNames; + + const int64_t loggingSerialNumber = + mVersionChangeTransaction->LoggingSerialNumber(); + const nsID& backgroundChildLoggingId = + mVersionChangeTransaction->GetLoggingInfo()->Id(); + + if (NS_WARN_IF(!mDatabase->RegisterTransaction(mVersionChangeTransaction))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!gConnectionPool) { + gConnectionPool = new ConnectionPool(); + } + + RefPtr<VersionChangeOp> versionChangeOp = new VersionChangeOp(this); + + uint64_t transactionId = + versionChangeOp->StartOnConnectionPool( + backgroundChildLoggingId, + mVersionChangeTransaction->DatabaseId(), + loggingSerialNumber, + objectStoreNames, + /* aIsWriteTransaction */ true); + + mVersionChangeOp = versionChangeOp; + + mVersionChangeTransaction->NoteActiveRequest(); + mVersionChangeTransaction->SetActive(transactionId); + + return NS_OK; +} + +nsresult +OpenDatabaseOp::SendUpgradeNeeded() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseWorkVersionChange); + MOZ_ASSERT(mVersionChangeTransaction); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + MOZ_ASSERT_IF(!IsActorDestroyed(), mDatabase); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + RefPtr<VersionChangeTransaction> transaction; + mVersionChangeTransaction.swap(transaction); + + nsresult rv = EnsureDatabaseActorIsAlive(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Transfer ownership to IPDL. + transaction->SetActorAlive(); + + if (!mDatabase->SendPBackgroundIDBVersionChangeTransactionConstructor( + transaction, + mMetadata->mCommonMetadata.version(), + mRequestedVersion, + mMetadata->mNextObjectStoreId, + mMetadata->mNextIndexId)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +void +OpenDatabaseOp::SendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT_IF(NS_SUCCEEDED(mResultCode), mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT_IF(NS_SUCCEEDED(mResultCode), !mVersionChangeTransaction); + + mMaybeBlockedDatabases.Clear(); + + DatabaseActorInfo* info; + if (gLiveDatabaseHashtable && + gLiveDatabaseHashtable->Get(mDatabaseId, &info) && + info->mWaitingFactoryOp) { + MOZ_ASSERT(info->mWaitingFactoryOp == this); + // SendResults() should only be called by Run() and Run() should only be + // called if there's a strong reference to the object that can't be cleared + // here, so it's safe to clear mWaitingFactoryOp without adding additional + // strong reference. + info->mWaitingFactoryOp = nullptr; + } + + if (mVersionChangeTransaction) { + MOZ_ASSERT(NS_FAILED(mResultCode)); + + mVersionChangeTransaction->Abort(mResultCode, /* aForce */ true); + mVersionChangeTransaction = nullptr; + } + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } else { + FactoryRequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + // If we just successfully completed a versionchange operation then we + // need to update the version in our metadata. + mMetadata->mCommonMetadata.version() = mRequestedVersion; + + nsresult rv = EnsureDatabaseActorIsAlive(); + if (NS_SUCCEEDED(rv)) { + // We successfully opened a database so use its actor as the success + // result for this request. + OpenDatabaseRequestResponse openResponse; + openResponse.databaseParent() = mDatabase; + response = openResponse; + } else { + response = ClampResultCode(rv); +#ifdef DEBUG + mResultCode = response.get_nsresult(); +#endif + } + } else { +#ifdef DEBUG + // If something failed then our metadata pointer is now bad. No one should + // ever touch it again though so just null it out in DEBUG builds to make + // sure we find such cases. + mMetadata = nullptr; +#endif + response = ClampResultCode(mResultCode); + } + + Unused << + PBackgroundIDBFactoryRequestParent::Send__delete__(this, response); + } + + if (mDatabase) { + MOZ_ASSERT(!mDirectoryLock); + + if (NS_FAILED(mResultCode)) { + mDatabase->Invalidate(); + } + + // Make sure to release the database on this thread. + mDatabase = nullptr; + } else if (mDirectoryLock) { + nsCOMPtr<nsIRunnable> callback = + NewRunnableMethod(this, &OpenDatabaseOp::ConnectionClosedCallback); + + RefPtr<WaitForTransactionsHelper> helper = + new WaitForTransactionsHelper(mDatabaseId, callback); + helper->WaitForTransactions(); + } + + FinishSendResults(); +} + +void +OpenDatabaseOp::ConnectionClosedCallback() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(mResultCode)); + MOZ_ASSERT(mDirectoryLock); + + mDirectoryLock = nullptr; +} + +void +OpenDatabaseOp::EnsureDatabaseActor() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::BeginVersionChange || + mState == State::DatabaseWorkVersionChange || + mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(!IsActorDestroyed()); + + if (mDatabase) { + return; + } + + MOZ_ASSERT(mMetadata->mDatabaseId.IsEmpty()); + mMetadata->mDatabaseId = mDatabaseId; + + MOZ_ASSERT(mMetadata->mFilePath.IsEmpty()); + mMetadata->mFilePath = mDatabaseFilePath; + + DatabaseActorInfo* info; + if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { + AssertMetadataConsistency(info->mMetadata); + mMetadata = info->mMetadata; + } + + auto factory = static_cast<Factory*>(Manager()); + + mDatabase = new Database(factory, + mCommonParams.principalInfo(), + mOptionalContentParentId, + mGroup, + mOrigin, + mTelemetryId, + mMetadata, + mFileManager, + mDirectoryLock.forget(), + mFileHandleDisabled, + mChromeWriteAccessAllowed); + + if (info) { + info->mLiveDatabases.AppendElement(mDatabase); + } else { + info = new DatabaseActorInfo(mMetadata, mDatabase); + gLiveDatabaseHashtable->Put(mDatabaseId, info); + } + + // Balanced in Database::CleanupMetadata(). + IncreaseBusyCount(); +} + +nsresult +OpenDatabaseOp::EnsureDatabaseActorIsAlive() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseWorkVersionChange || + mState == State::SendingResults); + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + MOZ_ASSERT(!IsActorDestroyed()); + + EnsureDatabaseActor(); + + if (mDatabase->IsActorAlive()) { + return NS_OK; + } + + auto factory = static_cast<Factory*>(Manager()); + + DatabaseSpec spec; + MetadataToSpec(spec); + + // Transfer ownership to IPDL. + mDatabase->SetActorAlive(); + + if (!factory->SendPBackgroundIDBDatabaseConstructor(mDatabase, spec, this)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +void +OpenDatabaseOp::MetadataToSpec(DatabaseSpec& aSpec) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + aSpec.metadata() = mMetadata->mCommonMetadata; + + for (auto objectStoreIter = mMetadata->mObjectStores.ConstIter(); + !objectStoreIter.Done(); + objectStoreIter.Next()) { + FullObjectStoreMetadata* metadata = objectStoreIter.UserData(); + MOZ_ASSERT(objectStoreIter.Key()); + MOZ_ASSERT(metadata); + + // XXX This should really be fallible... + ObjectStoreSpec* objectStoreSpec = aSpec.objectStores().AppendElement(); + objectStoreSpec->metadata() = metadata->mCommonMetadata; + + for (auto indexIter = metadata->mIndexes.Iter(); + !indexIter.Done(); + indexIter.Next()) { + FullIndexMetadata* indexMetadata = indexIter.UserData(); + MOZ_ASSERT(indexIter.Key()); + MOZ_ASSERT(indexMetadata); + + // XXX This should really be fallible... + IndexMetadata* metadata = objectStoreSpec->indexes().AppendElement(); + *metadata = indexMetadata->mCommonMetadata; + } + } +} + +#ifdef DEBUG + +void +OpenDatabaseOp::AssertMetadataConsistency(const FullDatabaseMetadata* aMetadata) +{ + AssertIsOnBackgroundThread(); + + const FullDatabaseMetadata* thisDB = mMetadata; + const FullDatabaseMetadata* otherDB = aMetadata; + + MOZ_ASSERT(thisDB); + MOZ_ASSERT(otherDB); + MOZ_ASSERT(thisDB != otherDB); + + MOZ_ASSERT(thisDB->mCommonMetadata.name() == otherDB->mCommonMetadata.name()); + MOZ_ASSERT(thisDB->mCommonMetadata.version() == + otherDB->mCommonMetadata.version()); + MOZ_ASSERT(thisDB->mCommonMetadata.persistenceType() == + otherDB->mCommonMetadata.persistenceType()); + MOZ_ASSERT(thisDB->mDatabaseId == otherDB->mDatabaseId); + MOZ_ASSERT(thisDB->mFilePath == otherDB->mFilePath); + + // |thisDB| reflects the latest objectStore and index ids that have committed + // to disk. The in-memory metadata |otherDB| keeps track of objectStores and + // indexes that were created and then removed as well, so the next ids for + // |otherDB| may be higher than for |thisDB|. + MOZ_ASSERT(thisDB->mNextObjectStoreId <= otherDB->mNextObjectStoreId); + MOZ_ASSERT(thisDB->mNextIndexId <= otherDB->mNextIndexId); + + MOZ_ASSERT(thisDB->mObjectStores.Count() == otherDB->mObjectStores.Count()); + + for (auto objectStoreIter = thisDB->mObjectStores.ConstIter(); + !objectStoreIter.Done(); + objectStoreIter.Next()) { + FullObjectStoreMetadata* thisObjectStore = objectStoreIter.UserData(); + MOZ_ASSERT(thisObjectStore); + MOZ_ASSERT(!thisObjectStore->mDeleted); + + auto* otherObjectStore = + MetadataNameOrIdMatcher<FullObjectStoreMetadata>::Match( + otherDB->mObjectStores, thisObjectStore->mCommonMetadata.id()); + MOZ_ASSERT(otherObjectStore); + + MOZ_ASSERT(thisObjectStore != otherObjectStore); + + MOZ_ASSERT(thisObjectStore->mCommonMetadata.id() == + otherObjectStore->mCommonMetadata.id()); + MOZ_ASSERT(thisObjectStore->mCommonMetadata.name() == + otherObjectStore->mCommonMetadata.name()); + MOZ_ASSERT(thisObjectStore->mCommonMetadata.autoIncrement() == + otherObjectStore->mCommonMetadata.autoIncrement()); + MOZ_ASSERT(thisObjectStore->mCommonMetadata.keyPath() == + otherObjectStore->mCommonMetadata.keyPath()); + // mNextAutoIncrementId and mCommittedAutoIncrementId may be modified + // concurrently with this OpenOp, so it is not possible to assert equality + // here. It's also possible that we've written the new ids to disk but not + // yet updated the in-memory count. + MOZ_ASSERT(thisObjectStore->mNextAutoIncrementId <= + otherObjectStore->mNextAutoIncrementId); + MOZ_ASSERT(thisObjectStore->mCommittedAutoIncrementId <= + otherObjectStore->mCommittedAutoIncrementId || + thisObjectStore->mCommittedAutoIncrementId == + otherObjectStore->mNextAutoIncrementId); + MOZ_ASSERT(!otherObjectStore->mDeleted); + + MOZ_ASSERT(thisObjectStore->mIndexes.Count() == + otherObjectStore->mIndexes.Count()); + + for (auto indexIter = thisObjectStore->mIndexes.Iter(); + !indexIter.Done(); + indexIter.Next()) { + FullIndexMetadata* thisIndex = indexIter.UserData(); + MOZ_ASSERT(thisIndex); + MOZ_ASSERT(!thisIndex->mDeleted); + + auto* otherIndex = + MetadataNameOrIdMatcher<FullIndexMetadata>:: + Match(otherObjectStore->mIndexes, thisIndex->mCommonMetadata.id()); + MOZ_ASSERT(otherIndex); + + MOZ_ASSERT(thisIndex != otherIndex); + + MOZ_ASSERT(thisIndex->mCommonMetadata.id() == + otherIndex->mCommonMetadata.id()); + MOZ_ASSERT(thisIndex->mCommonMetadata.name() == + otherIndex->mCommonMetadata.name()); + MOZ_ASSERT(thisIndex->mCommonMetadata.keyPath() == + otherIndex->mCommonMetadata.keyPath()); + MOZ_ASSERT(thisIndex->mCommonMetadata.unique() == + otherIndex->mCommonMetadata.unique()); + MOZ_ASSERT(thisIndex->mCommonMetadata.multiEntry() == + otherIndex->mCommonMetadata.multiEntry()); + MOZ_ASSERT(!otherIndex->mDeleted); + } + } +} + +#endif // DEBUG + +nsresult +OpenDatabaseOp:: +VersionChangeOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT(mOpenDatabaseOp->mState == State::DatabaseWorkVersionChange); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + PROFILER_LABEL("IndexedDB", + "OpenDatabaseOp::VersionChangeOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " + "Beginning database work", + "IndexedDB %s: P T[%lld]: DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mLoggingSerialNumber); + + Transaction()->SetActiveOnConnectionThread(); + + nsresult rv = aConnection->BeginWriteTransaction(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement updateStmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE database " + "SET version = :version;"), + &updateStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("version"), + int64_t(mRequestedVersion)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = updateStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +OpenDatabaseOp:: +VersionChangeOp::SendSuccessResult() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT(mOpenDatabaseOp->mState == State::DatabaseWorkVersionChange); + MOZ_ASSERT(mOpenDatabaseOp->mVersionChangeOp == this); + + nsresult rv = mOpenDatabaseOp->SendUpgradeNeeded(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool +OpenDatabaseOp:: +VersionChangeOp::SendFailureResult(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT(mOpenDatabaseOp->mState == State::DatabaseWorkVersionChange); + MOZ_ASSERT(mOpenDatabaseOp->mVersionChangeOp == this); + + mOpenDatabaseOp->SetFailureCode(aResultCode); + mOpenDatabaseOp->mState = State::SendingResults; + + MOZ_ALWAYS_SUCCEEDS(mOpenDatabaseOp->Run()); + + return false; +} + +void +OpenDatabaseOp:: +VersionChangeOp::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT(mOpenDatabaseOp->mVersionChangeOp == this); + + mOpenDatabaseOp->mVersionChangeOp = nullptr; + mOpenDatabaseOp = nullptr; + +#ifdef DEBUG + // A bit hacky but the VersionChangeOp is not generated in response to a + // child request like most other database operations. Do this to make our + // assertions happy. + NoteActorDestroyed(); +#endif + + TransactionDatabaseOperationBase::Cleanup(); +} + +void +DeleteDatabaseOp::LoadPreviousVersion(nsIFile* aDatabaseFile) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDatabaseFile); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + MOZ_ASSERT(!mPreviousVersion); + + PROFILER_LABEL("IndexedDB", + "DeleteDatabaseOp::LoadPreviousVersion", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + + nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<mozIStorageConnection> connection; + rv = OpenDatabaseAndHandleBusy(ss, aDatabaseFile, getter_AddRefs(connection)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + +#ifdef DEBUG + { + nsCOMPtr<mozIStorageStatement> stmt; + MOZ_ALWAYS_SUCCEEDS( + connection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT name " + "FROM database" + ), getter_AddRefs(stmt))); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + + nsString databaseName; + MOZ_ALWAYS_SUCCEEDS(stmt->GetString(0, databaseName)); + + MOZ_ASSERT(mCommonParams.metadata().name() == databaseName); + } +#endif + + nsCOMPtr<mozIStorageStatement> stmt; + rv = connection->CreateStatement(NS_LITERAL_CSTRING( + "SELECT version " + "FROM database" + ), getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (NS_WARN_IF(!hasResult)) { + return; + } + + int64_t version; + rv = stmt->GetInt64(0, &version); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + mPreviousVersion = uint64_t(version); +} + +nsresult +DeleteDatabaseOp::DatabaseOpen() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseOpenPending); + + // Swap this to the stack now to ensure that we release it on this thread. + RefPtr<ContentParent> contentParent; + mContentParent.swap(contentParent); + + nsresult rv = SendToIOThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DeleteDatabaseOp::DoDatabaseWork() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + + PROFILER_LABEL("IndexedDB", + "DeleteDatabaseOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const nsString& databaseName = mCommonParams.metadata().name(); + PersistenceType persistenceType = mCommonParams.metadata().persistenceType(); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsCOMPtr<nsIFile> directory; + nsresult rv = quotaManager->GetDirectoryForOrigin(persistenceType, + mOrigin, + getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = directory->Append(NS_LITERAL_STRING(IDB_DIRECTORY_NAME)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = directory->GetPath(mDatabaseDirectoryPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoString filename; + GetDatabaseFilename(databaseName, filename); + + mDatabaseFilenameBase = filename; + + nsCOMPtr<nsIFile> dbFile; + rv = directory->Clone(getter_AddRefs(dbFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = dbFile->Append(filename + NS_LITERAL_STRING(".sqlite")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + nsString databaseFilePath; + rv = dbFile->GetPath(databaseFilePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); +#endif + + bool exists; + rv = dbFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + // Parts of this function may fail but that shouldn't prevent us from + // deleting the file eventually. + LoadPreviousVersion(dbFile); + + mState = State::BeginVersionChange; + } else { + mState = State::SendingResults; + } + + rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DeleteDatabaseOp::BeginVersionChange() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::BeginVersionChange); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + DatabaseActorInfo* info; + if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { + MOZ_ASSERT(!info->mWaitingFactoryOp); + + NullableVersion newVersion = null_t(); + + nsresult rv = + SendVersionChangeMessages(info, nullptr, mPreviousVersion, newVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!mMaybeBlockedDatabases.IsEmpty()) { + info->mWaitingFactoryOp = this; + + mState = State::WaitingForOtherDatabasesToClose; + return NS_OK; + } + } + + // No other databases need to be notified, just make sure that all + // transactions are complete. + WaitForTransactions(); + return NS_OK; +} + +nsresult +DeleteDatabaseOp::DispatchToWorkThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForTransactionsToComplete); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mState = State::DatabaseWorkVersionChange; + + RefPtr<VersionChangeOp> versionChangeOp = new VersionChangeOp(this); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsresult rv = + quotaManager->IOThread()->Dispatch(versionChangeOp.forget(), + NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +void +DeleteDatabaseOp::NoteDatabaseClosed(Database* aDatabase) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); + MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); + + bool actorDestroyed = IsActorDestroyed(); + + nsresult rv; + if (actorDestroyed) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } else { + rv = NS_OK; + } + + // We are being called with an assuption that mWaitingFactoryOp holds a strong + // reference to us. + RefPtr<OpenDatabaseOp> kungFuDeathGrip; + + if (mMaybeBlockedDatabases.RemoveElement(aDatabase) && + mMaybeBlockedDatabases.IsEmpty()) { + if (actorDestroyed) { + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); + MOZ_ASSERT(info->mWaitingFactoryOp == this); + kungFuDeathGrip = + static_cast<OpenDatabaseOp*>(info->mWaitingFactoryOp.get()); + info->mWaitingFactoryOp = nullptr; + } else { + WaitForTransactions(); + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + // A strong reference is held in kungFuDeathGrip, so it's safe to call Run() + // directly. + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(Run()); + } +} + +void +DeleteDatabaseOp::SendBlockedNotification() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); + + if (!IsActorDestroyed()) { + Unused << SendBlocked(mPreviousVersion); + } +} + +void +DeleteDatabaseOp::SendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (!IsActorDestroyed()) { + FactoryRequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + response = DeleteDatabaseRequestResponse(mPreviousVersion); + } else { + response = ClampResultCode(mResultCode); + } + + Unused << + PBackgroundIDBFactoryRequestParent::Send__delete__(this, response); + } + + mDirectoryLock = nullptr; + + FinishSendResults(); +} + +nsresult +DeleteDatabaseOp:: +VersionChangeOp::DeleteFile(nsIFile* aDirectory, + const nsAString& aFilename, + QuotaManager* aQuotaManager) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(!aFilename.IsEmpty()); + MOZ_ASSERT_IF(aQuotaManager, mDeleteDatabaseOp->mEnforcingQuota); + + MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); + + PROFILER_LABEL("IndexedDB", + "DeleteDatabaseOp::VersionChangeOp::DeleteFile", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<nsIFile> file; + nsresult rv = aDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->Append(aFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t fileSize; + + if (aQuotaManager) { + rv = file->GetFileSize(&fileSize); + if (rv == NS_ERROR_FILE_NOT_FOUND || + rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(fileSize >= 0); + } + + rv = file->Remove(false); + if (rv == NS_ERROR_FILE_NOT_FOUND || + rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aQuotaManager && fileSize > 0) { + const PersistenceType& persistenceType = + mDeleteDatabaseOp->mCommonParams.metadata().persistenceType(); + + aQuotaManager->DecreaseUsageForOrigin(persistenceType, + mDeleteDatabaseOp->mGroup, + mDeleteDatabaseOp->mOrigin, + fileSize); + } + + return NS_OK; +} + +nsresult +DeleteDatabaseOp:: +VersionChangeOp::RunOnIOThread() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); + + PROFILER_LABEL("IndexedDB", + "DeleteDatabaseOp::VersionChangeOp::RunOnIOThread", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const PersistenceType& persistenceType = + mDeleteDatabaseOp->mCommonParams.metadata().persistenceType(); + + QuotaManager* quotaManager = + mDeleteDatabaseOp->mEnforcingQuota ? + QuotaManager::Get() : + nullptr; + + MOZ_ASSERT_IF(mDeleteDatabaseOp->mEnforcingQuota, quotaManager); + + nsCOMPtr<nsIFile> directory = + GetFileForPath(mDeleteDatabaseOp->mDatabaseDirectoryPath); + if (NS_WARN_IF(!directory)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // The database file counts towards quota. + nsAutoString filename = + mDeleteDatabaseOp->mDatabaseFilenameBase + NS_LITERAL_STRING(".sqlite"); + + nsresult rv = DeleteFile(directory, filename, quotaManager); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // .sqlite-journal files don't count towards quota. + const NS_ConvertASCIItoUTF16 journalSuffix( + kSQLiteJournalSuffix, + LiteralStringLength(kSQLiteJournalSuffix)); + + filename = mDeleteDatabaseOp->mDatabaseFilenameBase + journalSuffix; + + rv = DeleteFile(directory, filename, /* doesn't count */ nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // .sqlite-shm files don't count towards quota. + const NS_ConvertASCIItoUTF16 shmSuffix(kSQLiteSHMSuffix, + LiteralStringLength(kSQLiteSHMSuffix)); + + filename = mDeleteDatabaseOp->mDatabaseFilenameBase + shmSuffix; + + rv = DeleteFile(directory, filename, /* doesn't count */ nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // .sqlite-wal files do count towards quota. + const NS_ConvertASCIItoUTF16 walSuffix(kSQLiteWALSuffix, + LiteralStringLength(kSQLiteWALSuffix)); + + filename = mDeleteDatabaseOp->mDatabaseFilenameBase + walSuffix; + + rv = DeleteFile(directory, filename, quotaManager); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> fmDirectory; + rv = directory->Clone(getter_AddRefs(fmDirectory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The files directory counts towards quota. + const NS_ConvertASCIItoUTF16 filesSuffix( + kFileManagerDirectoryNameSuffix, + LiteralStringLength(kFileManagerDirectoryNameSuffix)); + + rv = fmDirectory->Append(mDeleteDatabaseOp->mDatabaseFilenameBase + + filesSuffix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = fmDirectory->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (exists) { + bool isDirectory; + rv = fmDirectory->IsDirectory(&isDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isDirectory)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + uint64_t usage = 0; + + if (mDeleteDatabaseOp->mEnforcingQuota) { + rv = FileManager::GetUsage(fmDirectory, &usage); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = fmDirectory->Remove(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We may have deleted some files, check if we can and update quota + // information before returning the error. + if (mDeleteDatabaseOp->mEnforcingQuota) { + uint64_t newUsage; + if (NS_SUCCEEDED(FileManager::GetUsage(fmDirectory, &newUsage))) { + MOZ_ASSERT(newUsage <= usage); + usage = usage - newUsage; + } + } + } + + if (mDeleteDatabaseOp->mEnforcingQuota && usage) { + quotaManager->DecreaseUsageForOrigin(persistenceType, + mDeleteDatabaseOp->mGroup, + mDeleteDatabaseOp->mOrigin, + usage); + } + + if (NS_FAILED(rv)) { + return rv; + } + } + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); + MOZ_ASSERT(mgr); + + const nsString& databaseName = + mDeleteDatabaseOp->mCommonParams.metadata().name(); + + mgr->InvalidateFileManager(persistenceType, + mDeleteDatabaseOp->mOrigin, + databaseName); + + rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +DeleteDatabaseOp:: +VersionChangeOp::RunOnOwningThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); + + RefPtr<DeleteDatabaseOp> deleteOp; + mDeleteDatabaseOp.swap(deleteOp); + + if (deleteOp->IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + deleteOp->SetFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } else { + DatabaseActorInfo* info; + if (gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId, &info) && + info->mWaitingFactoryOp) { + MOZ_ASSERT(info->mWaitingFactoryOp == deleteOp); + info->mWaitingFactoryOp = nullptr; + } + + if (NS_FAILED(mResultCode)) { + if (NS_SUCCEEDED(deleteOp->ResultCode())) { + deleteOp->SetFailureCode(mResultCode); + } + } else { + // Inform all the other databases that they are now invalidated. That + // should remove the previous metadata from our table. + if (info) { + MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); + + FallibleTArray<Database*> liveDatabases; + if (NS_WARN_IF(!liveDatabases.AppendElements(info->mLiveDatabases, + fallible))) { + deleteOp->SetFailureCode(NS_ERROR_OUT_OF_MEMORY); + } else { +#ifdef DEBUG + // The code below should result in the deletion of |info|. Set to null + // here to make sure we find invalid uses later. + info = nullptr; +#endif + for (uint32_t count = liveDatabases.Length(), index = 0; + index < count; + index++) { + RefPtr<Database> database = liveDatabases[index]; + database->Invalidate(); + } + + MOZ_ASSERT(!gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId)); + } + } + } + } + + // We hold a strong ref to the deleteOp, so it's safe to call Run() directly. + + deleteOp->mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(deleteOp->Run()); + +#ifdef DEBUG + // A bit hacky but the DeleteDatabaseOp::VersionChangeOp is not really a + // normal database operation that is tied to an actor. Do this to make our + // assertions happy. + NoteActorDestroyed(); +#endif +} + +nsresult +DeleteDatabaseOp:: +VersionChangeOp::Run() +{ + nsresult rv; + + if (IsOnIOThread()) { + rv = RunOnIOThread(); + } else { + RunOnOwningThread(); + rv = NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + return NS_OK; +} + +TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( + TransactionBase* aTransaction) + : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), + aTransaction->GetLoggingInfo()->NextRequestSN()) + , mTransaction(aTransaction) + , mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber()) + , mInternalState(InternalState::Initial) + , mTransactionIsAborted(aTransaction->IsAborted()) +{ + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(LoggingSerialNumber()); +} + +TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( + TransactionBase* aTransaction, + uint64_t aLoggingSerialNumber) + : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), + aLoggingSerialNumber) + , mTransaction(aTransaction) + , mTransactionLoggingSerialNumber(aTransaction->LoggingSerialNumber()) + , mInternalState(InternalState::Initial) + , mTransactionIsAborted(aTransaction->IsAborted()) +{ + MOZ_ASSERT(aTransaction); +} + +TransactionDatabaseOperationBase::~TransactionDatabaseOperationBase() +{ + MOZ_ASSERT(mInternalState == InternalState::Completed); + MOZ_ASSERT(!mTransaction, + "TransactionDatabaseOperationBase::Cleanup() was not called by a " + "subclass!"); +} + +#ifdef DEBUG + +void +TransactionDatabaseOperationBase::AssertIsOnConnectionThread() const +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); +} + +#endif // DEBUG + +uint64_t +TransactionDatabaseOperationBase::StartOnConnectionPool( + const nsID& aBackgroundChildLoggingId, + const nsACString& aDatabaseId, + int64_t aLoggingSerialNumber, + const nsTArray<nsString>& aObjectStoreNames, + bool aIsWriteTransaction) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::Initial); + + // Must set mInternalState before dispatching otherwise we will race with the + // connection thread. + mInternalState = InternalState::DatabaseWork; + + return gConnectionPool->Start(aBackgroundChildLoggingId, + aDatabaseId, + aLoggingSerialNumber, + aObjectStoreNames, + aIsWriteTransaction, + this); +} + +void +TransactionDatabaseOperationBase::DispatchToConnectionPool() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::Initial); + + Unused << this->Run(); +} + +void +TransactionDatabaseOperationBase::RunOnConnectionThread() +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mInternalState == InternalState::DatabaseWork); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + + PROFILER_LABEL("IndexedDB", + "TransactionDatabaseOperationBase::RunOnConnectionThread", + js::ProfileEntry::Category::STORAGE); + + // There are several cases where we don't actually have to to any work here. + + if (mTransactionIsAborted || mTransaction->IsInvalidatedOnAnyThread()) { + // This transaction is already set to be aborted or invalidated. + mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } else if (!OperationMayProceed()) { + // The operation was canceled in some way, likely because the child process + // has crashed. + IDB_REPORT_INTERNAL_ERR(); + mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } else { + Database* database = mTransaction->GetDatabase(); + MOZ_ASSERT(database); + + // Here we're actually going to perform the database operation. + nsresult rv = database->EnsureConnection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + mResultCode = rv; + } else { + DatabaseConnection* connection = database->GetConnection(); + MOZ_ASSERT(connection); + MOZ_ASSERT(connection->GetStorageConnection()); + + AutoSetProgressHandler autoProgress; + if (mLoggingSerialNumber) { + rv = autoProgress.Register(connection->GetStorageConnection(), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mResultCode = rv; + } + } + + if (NS_SUCCEEDED(rv)) { + if (mLoggingSerialNumber) { + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " + "Beginning database work", + "IndexedDB %s: P T[%lld] R[%llu]: DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransactionLoggingSerialNumber, + mLoggingSerialNumber); + } + + rv = DoDatabaseWork(connection); + + if (mLoggingSerialNumber) { + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " + "Finished database work", + "IndexedDB %s: P T[%lld] R[%llu]: DB End", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransactionLoggingSerialNumber, + mLoggingSerialNumber); + } + + if (NS_FAILED(rv)) { + mResultCode = rv; + } + } + } + } + + // Must set mInternalState before dispatching otherwise we will race with the + // owning thread. + if (HasPreprocessInfo()) { + mInternalState = InternalState::SendingPreprocess; + } else { + mInternalState = InternalState::SendingResults; + } + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +bool +TransactionDatabaseOperationBase::HasPreprocessInfo() +{ + return false; +} + +nsresult +TransactionDatabaseOperationBase::SendPreprocessInfo() +{ + return NS_OK; +} + +void +TransactionDatabaseOperationBase::NoteContinueReceived() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::WaitingForContinue); + + mInternalState = InternalState::SendingResults; + + // This TransactionDatabaseOperationBase can only be held alive by the IPDL. + // Run() can end up with clearing that last reference. So we need to add + // a self reference here. + RefPtr<TransactionDatabaseOperationBase> kungFuDeathGrip = this; + + Unused << this->Run(); +} + +void +TransactionDatabaseOperationBase::SendToConnectionPool() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::Initial); + + // Must set mInternalState before dispatching otherwise we will race with the + // connection thread. + mInternalState = InternalState::DatabaseWork; + + gConnectionPool->Dispatch(mTransaction->TransactionId(), this); + + mTransaction->NoteActiveRequest(); +} + +void +TransactionDatabaseOperationBase::SendPreprocess() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::SendingPreprocess); + + SendPreprocessInfoOrResults(/* aSendPreprocessInfo */ true); +} + +void +TransactionDatabaseOperationBase::SendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::SendingResults); + + SendPreprocessInfoOrResults(/* aSendPreprocessInfo */ false); +} + +void +TransactionDatabaseOperationBase::SendPreprocessInfoOrResults( + bool aSendPreprocessInfo) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::SendingPreprocess || + mInternalState == InternalState::SendingResults); + MOZ_ASSERT(mTransaction); + + if (NS_WARN_IF(IsActorDestroyed())) { + // Don't send any notifications if the actor was destroyed already. + if (NS_SUCCEEDED(mResultCode)) { + IDB_REPORT_INTERNAL_ERR(); + mResultCode = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } else { + if (mTransaction->IsInvalidated() || mTransaction->IsAborted()) { + // Aborted transactions always see their requests fail with ABORT_ERR, + // even if the request succeeded or failed with another error. + mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } else if (NS_SUCCEEDED(mResultCode)) { + if (aSendPreprocessInfo) { + // This should not release the IPDL reference. + mResultCode = SendPreprocessInfo(); + } else { + // This may release the IPDL reference. + mResultCode = SendSuccessResult(); + } + } + + if (NS_FAILED(mResultCode)) { + // This should definitely release the IPDL reference. + if (!SendFailureResult(mResultCode)) { + // Abort the transaction. + mTransaction->Abort(mResultCode, /* aForce */ false); + } + } + } + + if (aSendPreprocessInfo && NS_SUCCEEDED(mResultCode)) { + mInternalState = InternalState::WaitingForContinue; + } else { + if (mLoggingSerialNumber) { + mTransaction->NoteFinishedRequest(); + } + + Cleanup(); + + mInternalState = InternalState::Completed; + } +} + +bool +TransactionDatabaseOperationBase::Init(TransactionBase* aTransaction) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mInternalState == InternalState::Initial); + MOZ_ASSERT(aTransaction); + + return true; +} + +void +TransactionDatabaseOperationBase::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::SendingResults); + MOZ_ASSERT(mTransaction); + + mTransaction = nullptr; +} + +NS_IMETHODIMP +TransactionDatabaseOperationBase::Run() +{ + switch (mInternalState) { + case InternalState::Initial: + SendToConnectionPool(); + return NS_OK; + + case InternalState::DatabaseWork: + RunOnConnectionThread(); + return NS_OK; + + case InternalState::SendingPreprocess: + SendPreprocess(); + return NS_OK; + + case InternalState::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } +} + +TransactionBase:: +CommitOp::CommitOp(TransactionBase* aTransaction, nsresult aResultCode) + : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), + aTransaction->GetLoggingInfo()->NextRequestSN()) + , mTransaction(aTransaction) + , mResultCode(aResultCode) +{ + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(LoggingSerialNumber()); +} + +nsresult +TransactionBase:: +CommitOp::WriteAutoIncrementCounts() +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::READ_WRITE || + mTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || + mTransaction->GetMode() == IDBTransaction::CLEANUP || + mTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); + + const nsTArray<RefPtr<FullObjectStoreMetadata>>& metadataArray = + mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; + + if (!metadataArray.IsEmpty()) { + NS_NAMED_LITERAL_CSTRING(osid, "osid"); + NS_NAMED_LITERAL_CSTRING(ai, "ai"); + + Database* database = mTransaction->GetDatabase(); + MOZ_ASSERT(database); + + DatabaseConnection* connection = database->GetConnection(); + MOZ_ASSERT(connection); + + DatabaseConnection::CachedStatement stmt; + nsresult rv; + + for (uint32_t count = metadataArray.Length(), index = 0; + index < count; + index++) { + const RefPtr<FullObjectStoreMetadata>& metadata = metadataArray[index]; + MOZ_ASSERT(!metadata->mDeleted); + MOZ_ASSERT(metadata->mNextAutoIncrementId > 1); + + if (stmt) { + MOZ_ALWAYS_SUCCEEDS(stmt->Reset()); + } else { + rv = connection->GetCachedStatement( + NS_LITERAL_CSTRING("UPDATE object_store " + "SET auto_increment = :") + ai + + NS_LITERAL_CSTRING(" WHERE id = :") + osid + + NS_LITERAL_CSTRING(";"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = stmt->BindInt64ByName(osid, metadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(ai, metadata->mNextAutoIncrementId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + return NS_OK; +} + +void +TransactionBase:: +CommitOp::CommitOrRollbackAutoIncrementCounts() +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::READ_WRITE || + mTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH || + mTransaction->GetMode() == IDBTransaction::CLEANUP || + mTransaction->GetMode() == IDBTransaction::VERSION_CHANGE); + + nsTArray<RefPtr<FullObjectStoreMetadata>>& metadataArray = + mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; + + if (!metadataArray.IsEmpty()) { + bool committed = NS_SUCCEEDED(mResultCode); + + for (uint32_t count = metadataArray.Length(), index = 0; + index < count; + index++) { + RefPtr<FullObjectStoreMetadata>& metadata = metadataArray[index]; + + if (committed) { + metadata->mCommittedAutoIncrementId = metadata->mNextAutoIncrementId; + } else { + metadata->mNextAutoIncrementId = metadata->mCommittedAutoIncrementId; + } + } + } +} + +#ifdef DEBUG + +void +TransactionBase:: +CommitOp::AssertForeignKeyConsistency(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + MOZ_ASSERT(mTransaction->GetMode() != IDBTransaction::READ_ONLY); + + DatabaseConnection::CachedStatement pragmaStmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING("PRAGMA foreign_keys;"), + &pragmaStmt)); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(pragmaStmt->ExecuteStep(&hasResult)); + + MOZ_ASSERT(hasResult); + + int32_t foreignKeysEnabled; + MOZ_ALWAYS_SUCCEEDS(pragmaStmt->GetInt32(0, &foreignKeysEnabled)); + + MOZ_ASSERT(foreignKeysEnabled, "Database doesn't have foreign keys enabled!"); + + DatabaseConnection::CachedStatement checkStmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement( + NS_LITERAL_CSTRING("PRAGMA foreign_key_check;"), + &checkStmt)); + + MOZ_ALWAYS_SUCCEEDS(checkStmt->ExecuteStep(&hasResult)); + + MOZ_ASSERT(!hasResult, "Database has inconsisistent foreign keys!"); +} + +#endif // DEBUG + +NS_IMPL_ISUPPORTS_INHERITED0(TransactionBase::CommitOp, DatabaseOperationBase) + +NS_IMETHODIMP +TransactionBase:: +CommitOp::Run() +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "TransactionBase::CommitOp::Run", + js::ProfileEntry::Category::STORAGE); + + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " + "Beginning database work", + "IndexedDB %s: P T[%lld] R[%llu]: DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransaction->LoggingSerialNumber(), + mLoggingSerialNumber); + + if (mTransaction->GetMode() != IDBTransaction::READ_ONLY && + mTransaction->mHasBeenActiveOnConnectionThread) { + Database* database = mTransaction->GetDatabase(); + MOZ_ASSERT(database); + + if (DatabaseConnection* connection = database->GetConnection()) { + // May be null if the VersionChangeOp was canceled. + DatabaseConnection::UpdateRefcountFunction* fileRefcountFunction = + connection->GetUpdateRefcountFunction(); + + if (NS_SUCCEEDED(mResultCode)) { + if (fileRefcountFunction) { + mResultCode = fileRefcountFunction->WillCommit(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(mResultCode), + "WillCommit() failed!"); + } + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = WriteAutoIncrementCounts(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(mResultCode), + "WriteAutoIncrementCounts() failed!"); + + if (NS_SUCCEEDED(mResultCode)) { + AssertForeignKeyConsistency(connection); + + mResultCode = connection->CommitWriteTransaction(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(mResultCode), "Commit failed!"); + + if (NS_SUCCEEDED(mResultCode) && + mTransaction->GetMode() == IDBTransaction::READ_WRITE_FLUSH) { + mResultCode = connection->Checkpoint(); + } + + if (NS_SUCCEEDED(mResultCode) && fileRefcountFunction) { + fileRefcountFunction->DidCommit(); + } + } + } + } + + if (NS_FAILED(mResultCode)) { + if (fileRefcountFunction) { + fileRefcountFunction->DidAbort(); + } + + connection->RollbackWriteTransaction(); + } + + CommitOrRollbackAutoIncrementCounts(); + + connection->FinishWriteTransaction(); + + if (mTransaction->GetMode() == IDBTransaction::CLEANUP) { + connection->DoIdleProcessing(/* aNeedsCheckpoint */ true); + + connection->EnableQuotaChecks(); + } + } + } + + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld] Request[%llu]: " + "Finished database work", + "IndexedDB %s: P T[%lld] R[%llu]: DB End", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransaction->LoggingSerialNumber(), + mLoggingSerialNumber); + + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " + "Finished database work", + "IndexedDB %s: P T[%lld]: DB End", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mLoggingSerialNumber); + + return NS_OK; +} + +void +TransactionBase:: +CommitOp::TransactionFinishedBeforeUnblock() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction); + + PROFILER_LABEL("IndexedDB", + "CommitOp::TransactionFinishedBeforeUnblock", + js::ProfileEntry::Category::STORAGE); + + if (!IsActorDestroyed()) { + mTransaction->UpdateMetadata(mResultCode); + } +} + +void +TransactionBase:: +CommitOp::TransactionFinishedAfterUnblock() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction); + + IDB_LOG_MARK("IndexedDB %s: Parent Transaction[%lld]: " + "Finished with result 0x%x", + "IndexedDB %s: P T[%lld]: Transaction finished (0x%x)", + IDB_LOG_ID_STRING(mTransaction->GetLoggingInfo()->Id()), + mTransaction->LoggingSerialNumber(), + mResultCode); + + mTransaction->SendCompleteNotification(ClampResultCode(mResultCode)); + + Database* database = mTransaction->GetDatabase(); + MOZ_ASSERT(database); + + database->UnregisterTransaction(mTransaction); + + mTransaction = nullptr; + +#ifdef DEBUG + // A bit hacky but the CommitOp is not really a normal database operation + // that is tied to an actor. Do this to make our assertions happy. + NoteActorDestroyed(); +#endif +} + +DatabaseOp::DatabaseOp(Database* aDatabase) + : DatabaseOperationBase(aDatabase->GetLoggingInfo()->Id(), + aDatabase->GetLoggingInfo()->NextRequestSN()) + , mDatabase(aDatabase) + , mState(State::Initial) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); +} + +nsresult +DatabaseOp::SendToIOThread() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Initial); + + if (!OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (NS_WARN_IF(!quotaManager)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // Must set this before dispatching otherwise we will race with the IO thread. + mState = State::DatabaseWork; + + nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +NS_IMETHODIMP +DatabaseOp::Run() +{ + nsresult rv; + + switch (mState) { + case State::Initial: + rv = SendToIOThread(); + break; + + case State::DatabaseWork: + rv = DoDatabaseWork(); + break; + + case State::SendingResults: + SendResults(); + return NS_OK; + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = rv; + } + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingResults; + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + return NS_OK; +} + +void +DatabaseOp::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + + NoteActorDestroyed(); +} + +CreateFileOp::CreateFileOp(Database* aDatabase, + const DatabaseRequestParams& aParams) + : DatabaseOp(aDatabase) + , mParams(aParams.get_CreateFileParams()) +{ + MOZ_ASSERT(aParams.type() == DatabaseRequestParams::TCreateFileParams); +} + +nsresult +CreateFileOp::CreateMutableFile(MutableFile** aMutableFile) +{ + nsCOMPtr<nsIFile> file = GetFileForFileInfo(mFileInfo); + if (NS_WARN_IF(!file)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + RefPtr<MutableFile> mutableFile = + MutableFile::Create(file, mDatabase, mFileInfo); + if (NS_WARN_IF(!mutableFile)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + // Transfer ownership to IPDL. + mutableFile->SetActorAlive(); + + if (!mDatabase->SendPBackgroundMutableFileConstructor(mutableFile, + mParams.name(), + mParams.type())) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mutableFile.forget(aMutableFile); + return NS_OK; +} + +nsresult +CreateFileOp::DoDatabaseWork() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWork); + + PROFILER_LABEL("IndexedDB", + "CreateFileOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { + NS_WARNING("Refusing to create file because disk space is low!"); + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + + if (NS_WARN_IF(QuotaManager::IsShuttingDown()) || !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + FileManager* fileManager = mDatabase->GetFileManager(); + + mFileInfo = fileManager->GetNewFileInfo(); + if (NS_WARN_IF(!mFileInfo)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const int64_t fileId = mFileInfo->Id(); + + nsCOMPtr<nsIFile> journalDirectory = fileManager->EnsureJournalDirectory(); + if (NS_WARN_IF(!journalDirectory)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsCOMPtr<nsIFile> journalFile = + fileManager->GetFileForId(journalDirectory, fileId); + if (NS_WARN_IF(!journalFile)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsresult rv = journalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> fileDirectory = fileManager->GetDirectory(); + if (NS_WARN_IF(!fileDirectory)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsCOMPtr<nsIFile> file = fileManager->GetFileForId(fileDirectory, fileId); + if (NS_WARN_IF(!file)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = State::SendingResults; + + rv = mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +CreateFileOp::SendResults() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (!IsActorDestroyed() && !mDatabase->IsInvalidated()) { + DatabaseRequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + RefPtr<MutableFile> mutableFile; + nsresult rv = CreateMutableFile(getter_AddRefs(mutableFile)); + if (NS_SUCCEEDED(rv)) { + // We successfully created a mutable file so use its actor as the + // success result for this request. + CreateFileRequestResponse createResponse; + createResponse.mutableFileParent() = mutableFile; + response = createResponse; + } else { + response = ClampResultCode(rv); +#ifdef DEBUG + mResultCode = response.get_nsresult(); +#endif + } + } else { + response = ClampResultCode(mResultCode); + } + + Unused << + PBackgroundIDBDatabaseRequestParent::Send__delete__(this, response); + } + + mState = State::Completed; +} + +nsresult +VersionChangeTransactionOp::SendSuccessResult() +{ + AssertIsOnOwningThread(); + + // Nothing to send here, the API assumes that this request always succeeds. + return NS_OK; +} + +bool +VersionChangeTransactionOp::SendFailureResult(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + + // The only option here is to cause the transaction to abort. + return false; +} + +void +VersionChangeTransactionOp::Cleanup() +{ + AssertIsOnOwningThread(); + +#ifdef DEBUG + // A bit hacky but the VersionChangeTransactionOp is not generated in response + // to a child request like most other database operations. Do this to make our + // assertions happy. + NoteActorDestroyed(); +#endif + + TransactionDatabaseOperationBase::Cleanup(); +} + +nsresult +CreateObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "CreateObjectStoreOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + +#ifdef DEBUG + { + // Make sure that we're not creating an object store with the same name as + // another that already exists. This should be impossible because we should + // have thrown an error long before now... + DatabaseConnection::CachedStatement stmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT name " + "FROM object_store " + "WHERE name = :name;"), + &stmt)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name())); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + MOZ_ASSERT(!hasResult); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT INTO object_store (id, auto_increment, name, key_path) " + "VALUES (:id, :auto_increment, :name, :key_path);"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("auto_increment"), + mMetadata.autoIncrement() ? 1 : 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_NAMED_LITERAL_CSTRING(keyPath, "key_path"); + + if (mMetadata.keyPath().IsValid()) { + nsAutoString keyPathSerialization; + mMetadata.keyPath().SerializeToString(keyPathSerialization); + + rv = stmt->BindStringByName(keyPath, keyPathSerialization); + } else { + rv = stmt->BindNullByName(keyPath); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + { + int64_t id; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetStorageConnection()->GetLastInsertRowID(&id)); + MOZ_ASSERT(mMetadata.id() == id); + } +#endif + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DeleteObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "DeleteObjectStoreOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); + +#ifdef DEBUG + { + // Make sure |mIsLastObjectStore| is telling the truth. + DatabaseConnection::CachedStatement stmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT id " + "FROM object_store;"), + &stmt)); + + bool foundThisObjectStore = false; + bool foundOtherObjectStore = false; + + while (true) { + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + + if (!hasResult) { + break; + } + + int64_t id; + MOZ_ALWAYS_SUCCEEDS(stmt->GetInt64(0, &id)); + + if (id == mMetadata->mCommonMetadata.id()) { + foundThisObjectStore = true; + } else { + foundOtherObjectStore = true; + } + } + + MOZ_ASSERT_IF(mIsLastObjectStore, + foundThisObjectStore && !foundOtherObjectStore); + MOZ_ASSERT_IF(!mIsLastObjectStore, + foundThisObjectStore && foundOtherObjectStore); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mIsLastObjectStore) { + // We can just delete everything if this is the last object store. + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM index_data;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM unique_index_data;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_data;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_store_index;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_store;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + bool hasIndexes; + rv = ObjectStoreHasIndexes(aConnection, + mMetadata->mCommonMetadata.id(), + &hasIndexes); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasIndexes) { + rv = DeleteObjectStoreDataTableRowsWithIndexes( + aConnection, + mMetadata->mCommonMetadata.id(), + void_t()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now clean up the object store index table. + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_store_index " + "WHERE object_store_id = :object_store_id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(objectStoreIdString, + mMetadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // We only have to worry about object data if this object store has no + // indexes. + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_data " + "WHERE object_store_id = :object_store_id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(objectStoreIdString, + mMetadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_store " + "WHERE id = :object_store_id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(objectStoreIdString, + mMetadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + { + int32_t deletedRowCount; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetStorageConnection()-> + GetAffectedRows(&deletedRowCount)); + MOZ_ASSERT(deletedRowCount == 1); + } +#endif + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mMetadata->mCommonMetadata.autoIncrement()) { + Transaction()->ForgetModifiedAutoIncrementObjectStore(mMetadata); + } + + return NS_OK; +} + +nsresult +RenameObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "RenameObjectStoreOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + +#ifdef DEBUG + { + // Make sure that we're not renaming an object store with the same name as + // another that already exists. This should be impossible because we should + // have thrown an error long before now... + DatabaseConnection::CachedStatement stmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT name " + "FROM object_store " + "WHERE name = :name " + "AND id != :id;"), + &stmt)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mId)); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + MOZ_ASSERT(!hasResult); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE object_store " + "SET name = :name " + "WHERE id = :id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mId); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +CreateIndexOp::CreateIndexOp(VersionChangeTransaction* aTransaction, + const int64_t aObjectStoreId, + const IndexMetadata& aMetadata) + : VersionChangeTransactionOp(aTransaction) + , mMetadata(aMetadata) + , mFileManager(aTransaction->GetDatabase()->GetFileManager()) + , mDatabaseId(aTransaction->DatabaseId()) + , mObjectStoreId(aObjectStoreId) +{ + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(aMetadata.id()); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(!mDatabaseId.IsEmpty()); +} + +unsigned int CreateIndexOp::sThreadLocalIndex = kBadThreadLocalIndex; + +nsresult +CreateIndexOp::InsertDataFromObjectStore(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!IndexedDatabaseManager::InLowDiskSpaceMode()); + MOZ_ASSERT(mMaybeUniqueIndexTable); + + PROFILER_LABEL("IndexedDB", + "CreateIndexOp::InsertDataFromObjectStore", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<mozIStorageConnection> storageConnection = + aConnection->GetStorageConnection(); + MOZ_ASSERT(storageConnection); + + ThreadLocalJSContext* context = ThreadLocalJSContext::GetOrCreate(); + if (NS_WARN_IF(!context)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + JSContext* cx = context->Context(); + JSAutoRequest ar(cx); + JSAutoCompartment ac(cx, context->Global()); + + RefPtr<UpdateIndexDataValuesFunction> updateFunction = + new UpdateIndexDataValuesFunction(this, aConnection, cx); + + NS_NAMED_LITERAL_CSTRING(updateFunctionName, "update_index_data_values"); + + nsresult rv = + storageConnection->CreateFunction(updateFunctionName, + 4, + updateFunction); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = InsertDataFromObjectStoreInternal(aConnection); + + MOZ_ALWAYS_SUCCEEDS(storageConnection->RemoveFunction(updateFunctionName)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +CreateIndexOp::InsertDataFromObjectStoreInternal( + DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!IndexedDatabaseManager::InLowDiskSpaceMode()); + MOZ_ASSERT(mMaybeUniqueIndexTable); + + DebugOnly<void*> storageConnection = aConnection->GetStorageConnection(); + MOZ_ASSERT(storageConnection); + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE object_data " + "SET index_data_values = update_index_data_values " + "(key, index_data_values, file_ids, data) " + "WHERE object_store_id = :object_store_id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +bool +CreateIndexOp::Init(TransactionBase* aTransaction) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + + struct MOZ_STACK_CLASS Helper final + { + static void + Destroy(void* aThreadLocal) + { + delete static_cast<ThreadLocalJSContext*>(aThreadLocal); + } + }; + + if (sThreadLocalIndex == kBadThreadLocalIndex) { + if (NS_WARN_IF(PR_SUCCESS != + PR_NewThreadPrivateIndex(&sThreadLocalIndex, + &Helper::Destroy))) { + return false; + } + } + + MOZ_ASSERT(sThreadLocalIndex != kBadThreadLocalIndex); + + nsresult rv = + GetUniqueIndexTableForObjectStore(aTransaction, + mObjectStoreId, + mMaybeUniqueIndexTable); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +nsresult +CreateIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "CreateIndexOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + +#ifdef DEBUG + { + // Make sure that we're not creating an index with the same name and object + // store as another that already exists. This should be impossible because + // we should have thrown an error long before now... + DatabaseConnection::CachedStatement stmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT name " + "FROM object_store_index " + "WHERE object_store_id = :osid " + "AND name = :name;"), + &stmt)); + MOZ_ALWAYS_SUCCEEDS( + stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId)); + MOZ_ALWAYS_SUCCEEDS( + stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name())); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + + MOZ_ASSERT(!hasResult); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT INTO object_store_index (id, name, key_path, unique_index, " + "multientry, object_store_id, locale, " + "is_auto_locale) " + "VALUES (:id, :name, :key_path, :unique, :multientry, :osid, :locale, " + ":is_auto_locale)"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mMetadata.name()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoString keyPathSerialization; + mMetadata.keyPath().SerializeToString(keyPathSerialization); + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key_path"), + keyPathSerialization); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("unique"), + mMetadata.unique() ? 1 : 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("multientry"), + mMetadata.multiEntry() ? 1 : 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mMetadata.locale().IsEmpty()) { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("locale")); + } else { + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("locale"), + mMetadata.locale()); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("is_auto_locale"), + mMetadata.autoLocale()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + { + int64_t id; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetStorageConnection()->GetLastInsertRowID(&id)); + MOZ_ASSERT(mMetadata.id() == id); + } +#endif + + rv = InsertDataFromObjectStore(aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +static const JSClassOps sNormalJSContextGlobalClassOps = { + /* addProperty */ nullptr, + /* delProperty */ nullptr, + /* getProperty */ nullptr, + /* setProperty */ nullptr, + /* enumerate */ nullptr, + /* resolve */ nullptr, + /* mayResolve */ nullptr, + /* finalize */ nullptr, + /* call */ nullptr, + /* hasInstance */ nullptr, + /* construct */ nullptr, + /* trace */ JS_GlobalObjectTraceHook +}; + +const JSClass NormalJSContext::sGlobalClass = { + "IndexedDBTransactionThreadGlobal", + JSCLASS_GLOBAL_FLAGS, + &sNormalJSContextGlobalClassOps +}; + +bool +NormalJSContext::Init() +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + + mContext = JS_NewContext(kContextHeapSize); + if (NS_WARN_IF(!mContext)) { + return false; + } + + // Let everyone know that we might be able to call JS. This alerts the + // profiler about certain possible deadlocks. + NS_GetCurrentThread()->SetCanInvokeJS(true); + + // Not setting this will cause JS_CHECK_RECURSION to report false positives. + JS_SetNativeStackQuota(mContext, 128 * sizeof(size_t) * 1024); + + if (NS_WARN_IF(!JS::InitSelfHostedCode(mContext))) { + return false; + } + + JSAutoRequest ar(mContext); + + JS::CompartmentOptions options; + mGlobal = JS_NewGlobalObject(mContext, &sGlobalClass, nullptr, + JS::FireOnNewGlobalHook, options); + if (NS_WARN_IF(!mGlobal)) { + return false; + } + + return true; +} + +// static +NormalJSContext* +NormalJSContext::Create() +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + + nsAutoPtr<NormalJSContext> newContext(new NormalJSContext()); + + if (NS_WARN_IF(!newContext->Init())) { + return nullptr; + } + + return newContext.forget(); +} + +// static +auto +CreateIndexOp:: +ThreadLocalJSContext::GetOrCreate() -> ThreadLocalJSContext* +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(CreateIndexOp::kBadThreadLocalIndex != + CreateIndexOp::sThreadLocalIndex); + + auto* context = static_cast<ThreadLocalJSContext*>( + PR_GetThreadPrivate(CreateIndexOp::sThreadLocalIndex)); + if (context) { + return context; + } + + nsAutoPtr<ThreadLocalJSContext> newContext(new ThreadLocalJSContext()); + + if (NS_WARN_IF(!newContext->Init())) { + return nullptr; + } + + DebugOnly<PRStatus> status = + PR_SetThreadPrivate(CreateIndexOp::sThreadLocalIndex, newContext); + MOZ_ASSERT(status == PR_SUCCESS); + + return newContext.forget(); +} + +NS_IMPL_ISUPPORTS(CreateIndexOp::UpdateIndexDataValuesFunction, + mozIStorageFunction); + +NS_IMETHODIMP +CreateIndexOp:: +UpdateIndexDataValuesFunction::OnFunctionCall(mozIStorageValueArray* aValues, + nsIVariant** _retval) +{ + MOZ_ASSERT(aValues); + MOZ_ASSERT(_retval); + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mOp); + MOZ_ASSERT(mCx); + + PROFILER_LABEL("IndexedDB", + "CreateIndexOp::UpdateIndexDataValuesFunction::OnFunctionCall", + js::ProfileEntry::Category::STORAGE); + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 4); // key, index_data_values, file_ids, data + + int32_t valueType; + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(1, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || + valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(2, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || + valueType == mozIStorageValueArray::VALUE_TYPE_TEXT); + + MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(3, &valueType)); + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB || + valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER); + } +#endif + + StructuredCloneReadInfo cloneInfo; + nsresult rv = + GetStructuredCloneReadInfoFromValueArray(aValues, + /* aDataIndex */ 3, + /* aFileIdsIndex */ 2, + mOp->mFileManager, + &cloneInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JS::Rooted<JS::Value> clone(mCx); + if (NS_WARN_IF(!IDBObjectStore::DeserializeIndexValue(mCx, + cloneInfo, + &clone))) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + const IndexMetadata& metadata = mOp->mMetadata; + const int64_t& objectStoreId = mOp->mObjectStoreId; + + AutoTArray<IndexUpdateInfo, 32> updateInfos; + rv = IDBObjectStore::AppendIndexUpdateInfo(metadata.id(), + metadata.keyPath(), + metadata.unique(), + metadata.multiEntry(), + metadata.locale(), + mCx, + clone, + updateInfos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (updateInfos.IsEmpty()) { + // XXX See if we can do this without copying... + + nsCOMPtr<nsIVariant> unmodifiedValue; + + // No changes needed, just return the original value. + int32_t valueType; + rv = aValues->GetTypeOfIndex(1, &valueType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL || + valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + + if (valueType == mozIStorageValueArray::VALUE_TYPE_NULL) { + unmodifiedValue = new storage::NullVariant(); + unmodifiedValue.forget(_retval); + return NS_OK; + } + + MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB); + + const uint8_t* blobData; + uint32_t blobDataLength; + rv = aValues->GetSharedBlob(1, &blobDataLength, &blobData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + std::pair<uint8_t *, int> copiedBlobDataPair( + static_cast<uint8_t*>(malloc(blobDataLength)), + blobDataLength); + + if (!copiedBlobDataPair.first) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy(copiedBlobDataPair.first, blobData, blobDataLength); + + unmodifiedValue = new storage::AdoptedBlobVariant(copiedBlobDataPair); + unmodifiedValue.forget(_retval); + + return NS_OK; + } + + Key key; + rv = key.SetFromValueArray(aValues, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AutoTArray<IndexDataValue, 32> indexValues; + rv = ReadCompressedIndexDataValues(aValues, 1, indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + const bool hadPreviousIndexValues = !indexValues.IsEmpty(); + + const uint32_t updateInfoCount = updateInfos.Length(); + + if (NS_WARN_IF(!indexValues.SetCapacity(indexValues.Length() + + updateInfoCount, fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + // First construct the full list to update the index_data_values row. + for (uint32_t index = 0; index < updateInfoCount; index++) { + const IndexUpdateInfo& info = updateInfos[index]; + + MOZ_ALWAYS_TRUE( + indexValues.InsertElementSorted(IndexDataValue(metadata.id(), + metadata.unique(), + info.value(), + info.localizedValue()), + fallible)); + } + + UniqueFreePtr<uint8_t> indexValuesBlob; + uint32_t indexValuesBlobLength; + rv = MakeCompressedIndexDataValues(indexValues, + indexValuesBlob, + &indexValuesBlobLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!indexValuesBlobLength == !(indexValuesBlob.get())); + + nsCOMPtr<nsIVariant> value; + + if (!indexValuesBlob) { + value = new storage::NullVariant(); + + value.forget(_retval); + return NS_OK; + } + + // Now insert the new table rows. We only need to construct a new list if + // the full list is different. + if (hadPreviousIndexValues) { + indexValues.ClearAndRetainStorage(); + + MOZ_ASSERT(indexValues.Capacity() >= updateInfoCount); + + for (uint32_t index = 0; index < updateInfoCount; index++) { + const IndexUpdateInfo& info = updateInfos[index]; + + MOZ_ALWAYS_TRUE( + indexValues.InsertElementSorted(IndexDataValue(metadata.id(), + metadata.unique(), + info.value(), + info.localizedValue()), + fallible)); + } + } + + rv = InsertIndexTableRows(mConnection, objectStoreId, key, indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + std::pair<uint8_t *, int> copiedBlobDataPair(indexValuesBlob.release(), + indexValuesBlobLength); + + value = new storage::AdoptedBlobVariant(copiedBlobDataPair); + + value.forget(_retval); + return NS_OK; +} + +DeleteIndexOp::DeleteIndexOp(VersionChangeTransaction* aTransaction, + const int64_t aObjectStoreId, + const int64_t aIndexId, + const bool aUnique, + const bool aIsLastIndex) + : VersionChangeTransactionOp(aTransaction) + , mObjectStoreId(aObjectStoreId) + , mIndexId(aIndexId) + , mUnique(aUnique) + , mIsLastIndex(aIsLastIndex) +{ + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(aIndexId); +} + +nsresult +DeleteIndexOp::RemoveReferencesToIndex(DatabaseConnection* aConnection, + const Key& aObjectStoreKey, + nsTArray<IndexDataValue>& aIndexValues) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + MOZ_ASSERT_IF(!mIsLastIndex, !aIndexValues.IsEmpty()); + + struct MOZ_STACK_CLASS IndexIdComparator final + { + bool + Equals(const IndexDataValue& aA, const IndexDataValue& aB) const + { + // Ignore everything but the index id. + return aA.mIndexId == aB.mIndexId; + }; + + bool + LessThan(const IndexDataValue& aA, const IndexDataValue& aB) const + { + return aA.mIndexId < aB.mIndexId; + }; + }; + + PROFILER_LABEL("IndexedDB", + "DeleteIndexOp::RemoveReferencesToIndex", + js::ProfileEntry::Category::STORAGE); + + if (mIsLastIndex) { + // There is no need to parse the previous entry in the index_data_values + // column if this is the last index. Simply set it to NULL. + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE object_data " + "SET index_data_values = NULL " + "WHERE object_store_id = :object_store_id " + "AND key = :key;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aObjectStoreKey.BindToStatement(stmt, NS_LITERAL_CSTRING("key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + IndexDataValue search; + search.mIndexId = mIndexId; + + // This returns the first element that matches our index id found during a + // binary search. However, there could still be other elements before that. + size_t firstElementIndex = + aIndexValues.BinaryIndexOf(search, IndexIdComparator()); + if (NS_WARN_IF(firstElementIndex == aIndexValues.NoIndex) || + NS_WARN_IF(aIndexValues[firstElementIndex].mIndexId != mIndexId)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + MOZ_ASSERT(aIndexValues[firstElementIndex].mIndexId == mIndexId); + + // Walk backwards to find the real first index. + while (firstElementIndex) { + if (aIndexValues[firstElementIndex - 1].mIndexId == mIndexId) { + firstElementIndex--; + } else { + break; + } + } + + MOZ_ASSERT(aIndexValues[firstElementIndex].mIndexId == mIndexId); + + const size_t indexValuesLength = aIndexValues.Length(); + + // Find the last element with the same index id. + size_t lastElementIndex = firstElementIndex; + + while (lastElementIndex < indexValuesLength) { + if (aIndexValues[lastElementIndex].mIndexId == mIndexId) { + lastElementIndex++; + } else { + break; + } + } + + MOZ_ASSERT(lastElementIndex > firstElementIndex); + MOZ_ASSERT_IF(lastElementIndex < indexValuesLength, + aIndexValues[lastElementIndex].mIndexId != mIndexId); + MOZ_ASSERT(aIndexValues[lastElementIndex - 1].mIndexId == mIndexId); + + aIndexValues.RemoveElementsAt(firstElementIndex, + lastElementIndex - firstElementIndex); + + nsresult rv = UpdateIndexValues(aConnection, + mObjectStoreId, + aObjectStoreKey, + aIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +DeleteIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + +#ifdef DEBUG + { + // Make sure |mIsLastIndex| is telling the truth. + DatabaseConnection::CachedStatement stmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT id " + "FROM object_store_index " + "WHERE object_store_id = :object_store_id;"), + &stmt)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mObjectStoreId)); + + bool foundThisIndex = false; + bool foundOtherIndex = false; + + while (true) { + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + + if (!hasResult) { + break; + } + + int64_t id; + MOZ_ALWAYS_SUCCEEDS(stmt->GetInt64(0, &id)); + + if (id == mIndexId) { + foundThisIndex = true; + } else { + foundOtherIndex = true; + } + } + + MOZ_ASSERT_IF(mIsLastIndex, foundThisIndex && !foundOtherIndex); + MOZ_ASSERT_IF(!mIsLastIndex, foundThisIndex && foundOtherIndex); + } +#endif + + PROFILER_LABEL("IndexedDB", + "DeleteIndexOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement selectStmt; + + // mozStorage warns that these statements trigger a sort operation but we + // don't care because this is a very rare call and we expect it to be slow. + // The cost of having an index on this field is too high. + if (mUnique) { + if (mIsLastIndex) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "/* do not warn (bug someone else) */ " + "SELECT value, object_data_key " + "FROM unique_index_data " + "WHERE index_id = :index_id " + "ORDER BY object_data_key ASC;"), + &selectStmt); + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "/* do not warn (bug out) */ " + "SELECT unique_index_data.value, " + "unique_index_data.object_data_key, " + "object_data.index_data_values " + "FROM unique_index_data " + "JOIN object_data " + "ON unique_index_data.object_data_key = object_data.key " + "WHERE unique_index_data.index_id = :index_id " + "AND object_data.object_store_id = :object_store_id " + "ORDER BY unique_index_data.object_data_key ASC;"), + &selectStmt); + } + } else { + if (mIsLastIndex) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "/* do not warn (bug me not) */ " + "SELECT value, object_data_key " + "FROM index_data " + "WHERE index_id = :index_id " + "AND object_store_id = :object_store_id " + "ORDER BY object_data_key ASC;"), + &selectStmt); + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "/* do not warn (bug off) */ " + "SELECT index_data.value, " + "index_data.object_data_key, " + "object_data.index_data_values " + "FROM index_data " + "JOIN object_data " + "ON index_data.object_data_key = object_data.key " + "WHERE index_data.index_id = :index_id " + "AND object_data.object_store_id = :object_store_id " + "ORDER BY index_data.object_data_key ASC;"), + &selectStmt); + } + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_NAMED_LITERAL_CSTRING(indexIdString, "index_id"); + + rv = selectStmt->BindInt64ByName(indexIdString, mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!mUnique || !mIsLastIndex) { + rv = selectStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + NS_NAMED_LITERAL_CSTRING(valueString, "value"); + NS_NAMED_LITERAL_CSTRING(objectDataKeyString, "object_data_key"); + + DatabaseConnection::CachedStatement deleteIndexRowStmt; + DatabaseConnection::CachedStatement nullIndexDataValuesStmt; + + Key lastObjectStoreKey; + AutoTArray<IndexDataValue, 32> lastIndexValues; + + bool hasResult; + while (NS_SUCCEEDED(rv = selectStmt->ExecuteStep(&hasResult)) && hasResult) { + // We always need the index key to delete the index row. + Key indexKey; + rv = indexKey.SetFromStatement(selectStmt, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(indexKey.IsUnset())) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + // Don't call |lastObjectStoreKey.BindToStatement()| directly because we + // don't want to copy the same key multiple times. + const uint8_t* objectStoreKeyData; + uint32_t objectStoreKeyDataLength; + rv = selectStmt->GetSharedBlob(1, + &objectStoreKeyDataLength, + &objectStoreKeyData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!objectStoreKeyDataLength)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + nsDependentCString currentObjectStoreKeyBuffer( + reinterpret_cast<const char*>(objectStoreKeyData), + objectStoreKeyDataLength); + if (currentObjectStoreKeyBuffer != lastObjectStoreKey.GetBuffer()) { + // We just walked to the next object store key. + if (!lastObjectStoreKey.IsUnset()) { + // Before we move on to the next key we need to update the previous + // key's index_data_values column. + rv = RemoveReferencesToIndex(aConnection, + lastObjectStoreKey, + lastIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Save the object store key. + lastObjectStoreKey = Key(currentObjectStoreKeyBuffer); + + // And the |index_data_values| row if this isn't the only index. + if (!mIsLastIndex) { + lastIndexValues.ClearAndRetainStorage(); + rv = ReadCompressedIndexDataValues(selectStmt, 2, lastIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(lastIndexValues.IsEmpty())) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + } + } + + // Now delete the index row. + if (deleteIndexRowStmt) { + MOZ_ALWAYS_SUCCEEDS(deleteIndexRowStmt->Reset()); + } else { + if (mUnique) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM unique_index_data " + "WHERE index_id = :index_id " + "AND value = :value;"), + &deleteIndexRowStmt); + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM index_data " + "WHERE index_id = :index_id " + "AND value = :value " + "AND object_data_key = :object_data_key;"), + &deleteIndexRowStmt); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = deleteIndexRowStmt->BindInt64ByName(indexIdString, mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = indexKey.BindToStatement(deleteIndexRowStmt, valueString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!mUnique) { + rv = lastObjectStoreKey.BindToStatement(deleteIndexRowStmt, + objectDataKeyString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = deleteIndexRowStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Take care of the last key. + if (!lastObjectStoreKey.IsUnset()) { + MOZ_ASSERT_IF(!mIsLastIndex, !lastIndexValues.IsEmpty()); + + rv = RemoveReferencesToIndex(aConnection, + lastObjectStoreKey, + lastIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + DatabaseConnection::CachedStatement deleteStmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_store_index " + "WHERE id = :index_id;"), + &deleteStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = deleteStmt->BindInt64ByName(indexIdString, mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = deleteStmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + { + int32_t deletedRowCount; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetStorageConnection()->GetAffectedRows(&deletedRowCount)); + MOZ_ASSERT(deletedRowCount == 1); + } +#endif + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +RenameIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "RenameIndexOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + +#ifdef DEBUG + { + // Make sure that we're not renaming an index with the same name as another + // that already exists. This should be impossible because we should have + // thrown an error long before now... + DatabaseConnection::CachedStatement stmt; + MOZ_ALWAYS_SUCCEEDS( + aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT name " + "FROM object_store_index " + "WHERE object_store_id = :object_store_id " + "AND name = :name " + "AND id != :id;"), + &stmt)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mObjectStoreId)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName)); + + MOZ_ALWAYS_SUCCEEDS( + stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mIndexId)); + + bool hasResult; + MOZ_ALWAYS_SUCCEEDS(stmt->ExecuteStep(&hasResult)); + MOZ_ASSERT(!hasResult); + } +#else + Unused << mObjectStoreId; +#endif + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "UPDATE object_store_index " + "SET name = :name " + "WHERE id = :id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("name"), mNewName); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mIndexId); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + + return NS_OK; +} + +// static +nsresult +NormalTransactionOp::ObjectStoreHasIndexes(NormalTransactionOp* aOp, + DatabaseConnection* aConnection, + const int64_t aObjectStoreId, + const bool aMayHaveIndexes, + bool* aHasIndexes) +{ + MOZ_ASSERT(aOp); + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(aHasIndexes); + + bool hasIndexes; + if (aOp->Transaction()->GetMode() == IDBTransaction::VERSION_CHANGE && + aMayHaveIndexes) { + // If this is a version change transaction then mObjectStoreMayHaveIndexes + // could be wrong (e.g. if a unique index failed to be created due to a + // constraint error). We have to check on this thread by asking the database + // directly. + nsresult rv = + DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, + aObjectStoreId, + &hasIndexes); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + MOZ_ASSERT(NS_SUCCEEDED( + DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, + aObjectStoreId, + &hasIndexes))); + MOZ_ASSERT(aMayHaveIndexes == hasIndexes); + + hasIndexes = aMayHaveIndexes; + } + + *aHasIndexes = hasIndexes; + return NS_OK; +} + +nsresult +NormalTransactionOp::GetPreprocessParams(PreprocessParams& aParams) +{ + return NS_OK; +} + +nsresult +NormalTransactionOp::SendPreprocessInfo() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + PreprocessParams params; + nsresult rv = GetPreprocessParams(params); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(params.type() != PreprocessParams::T__None); + + if (NS_WARN_IF(!PBackgroundIDBRequestParent::SendPreprocess(params))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +nsresult +NormalTransactionOp::SendSuccessResult() +{ + AssertIsOnOwningThread(); + + if (!IsActorDestroyed()) { + RequestResponse response; + GetResponse(response); + + MOZ_ASSERT(response.type() != RequestResponse::T__None); + + if (response.type() == RequestResponse::Tnsresult) { + MOZ_ASSERT(NS_FAILED(response.get_nsresult())); + + return response.get_nsresult(); + } + + if (NS_WARN_IF(!PBackgroundIDBRequestParent::Send__delete__(this, + response))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + +#ifdef DEBUG + mResponseSent = true; +#endif + + return NS_OK; +} + +bool +NormalTransactionOp::SendFailureResult(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); + + bool result = false; + + if (!IsActorDestroyed()) { + result = + PBackgroundIDBRequestParent::Send__delete__(this, + ClampResultCode(aResultCode)); + } + +#ifdef DEBUG + mResponseSent = true; +#endif + + return result; +} + +void +NormalTransactionOp::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(!IsActorDestroyed(), mResponseSent); + + TransactionDatabaseOperationBase::Cleanup(); +} + +void +NormalTransactionOp::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +bool +NormalTransactionOp::RecvContinue(const PreprocessResponse& aResponse) +{ + AssertIsOnOwningThread(); + + switch (aResponse.type()) { + case PreprocessResponse::Tnsresult: + mResultCode = aResponse.get_nsresult(); + break; + + case PreprocessResponse::TObjectStoreGetPreprocessResponse: + break; + + case PreprocessResponse::TObjectStoreGetAllPreprocessResponse: + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + NoteContinueReceived(); + + return true; +} + +ObjectStoreAddOrPutRequestOp::ObjectStoreAddOrPutRequestOp( + TransactionBase* aTransaction, + const RequestParams& aParams) + : NormalTransactionOp(aTransaction) + , mParams(aParams.type() == RequestParams::TObjectStoreAddParams ? + aParams.get_ObjectStoreAddParams().commonParams() : + aParams.get_ObjectStorePutParams().commonParams()) + , mGroup(aTransaction->GetDatabase()->Group()) + , mOrigin(aTransaction->GetDatabase()->Origin()) + , mPersistenceType(aTransaction->GetDatabase()->Type()) + , mOverwrite(aParams.type() == RequestParams::TObjectStorePutParams) + , mObjectStoreMayHaveIndexes(false) +{ + MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreAddParams || + aParams.type() == RequestParams::TObjectStorePutParams); + + mMetadata = + aTransaction->GetMetadataForObjectStoreId(mParams.objectStoreId()); + MOZ_ASSERT(mMetadata); + + mObjectStoreMayHaveIndexes = mMetadata->HasLiveIndexes(); + + mDataOverThreshold = + snappy::MaxCompressedLength(mParams.cloneInfo().data().data.Size()) > + IndexedDatabaseManager::DataThreshold(); +} + +nsresult +ObjectStoreAddOrPutRequestOp::RemoveOldIndexDataValues( + DatabaseConnection* aConnection) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(mOverwrite); + MOZ_ASSERT(!mResponse.IsUnset()); + +#ifdef DEBUG + { + bool hasIndexes = false; + MOZ_ASSERT(NS_SUCCEEDED( + DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, + mParams.objectStoreId(), + &hasIndexes))); + MOZ_ASSERT(hasIndexes, + "Don't use this slow method if there are no indexes!"); + } +#endif + + DatabaseConnection::CachedStatement indexValuesStmt; + nsresult rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "SELECT index_data_values " + "FROM object_data " + "WHERE object_store_id = :object_store_id " + "AND key = :key;"), + &indexValuesStmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = indexValuesStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mParams.objectStoreId()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mResponse.BindToStatement(indexValuesStmt, NS_LITERAL_CSTRING("key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool hasResult; + rv = indexValuesStmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasResult) { + AutoTArray<IndexDataValue, 32> existingIndexValues; + rv = ReadCompressedIndexDataValues(indexValuesStmt, + 0, + existingIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = DeleteIndexDataTableRows(aConnection, mResponse, existingIndexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +bool +ObjectStoreAddOrPutRequestOp::Init(TransactionBase* aTransaction) +{ + AssertIsOnOwningThread(); + + const nsTArray<IndexUpdateInfo>& indexUpdateInfos = + mParams.indexUpdateInfos(); + + if (!indexUpdateInfos.IsEmpty()) { + const uint32_t count = indexUpdateInfos.Length(); + + mUniqueIndexTable.emplace(); + + for (uint32_t index = 0; index < count; index++) { + const IndexUpdateInfo& updateInfo = indexUpdateInfos[index]; + + RefPtr<FullIndexMetadata> indexMetadata; + MOZ_ALWAYS_TRUE(mMetadata->mIndexes.Get(updateInfo.indexId(), + getter_AddRefs(indexMetadata))); + + MOZ_ASSERT(!indexMetadata->mDeleted); + + const int64_t& indexId = indexMetadata->mCommonMetadata.id(); + const bool& unique = indexMetadata->mCommonMetadata.unique(); + + MOZ_ASSERT(indexId == updateInfo.indexId()); + MOZ_ASSERT_IF(!indexMetadata->mCommonMetadata.multiEntry(), + !mUniqueIndexTable.ref().Get(indexId)); + + if (NS_WARN_IF(!mUniqueIndexTable.ref().Put(indexId, unique, fallible))) { + return false; + } + } + } else if (mOverwrite) { + mUniqueIndexTable.emplace(); + } + +#ifdef DEBUG + if (mUniqueIndexTable.isSome()) { + mUniqueIndexTable.ref().MarkImmutable(); + } +#endif + + const nsTArray<FileAddInfo>& fileAddInfos = mParams.fileAddInfos(); + + if (!fileAddInfos.IsEmpty()) { + const uint32_t count = fileAddInfos.Length(); + + if (NS_WARN_IF(!mStoredFileInfos.SetCapacity(count, fallible))) { + return false; + } + + for (uint32_t index = 0; index < count; index++) { + const FileAddInfo& fileAddInfo = fileAddInfos[index]; + + MOZ_ASSERT(fileAddInfo.type() == StructuredCloneFile::eBlob || + fileAddInfo.type() == StructuredCloneFile::eMutableFile || + fileAddInfo.type() == StructuredCloneFile::eWasmBytecode || + fileAddInfo.type() == StructuredCloneFile::eWasmCompiled); + + const DatabaseOrMutableFile& file = fileAddInfo.file(); + + StoredFileInfo* storedFileInfo = mStoredFileInfos.AppendElement(fallible); + MOZ_ASSERT(storedFileInfo); + + switch (fileAddInfo.type()) { + case StructuredCloneFile::eBlob: { + MOZ_ASSERT(file.type() == + DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent); + + storedFileInfo->mFileActor = + static_cast<DatabaseFile*>( + file.get_PBackgroundIDBDatabaseFileParent()); + MOZ_ASSERT(storedFileInfo->mFileActor); + + storedFileInfo->mFileInfo = storedFileInfo->mFileActor->GetFileInfo(); + MOZ_ASSERT(storedFileInfo->mFileInfo); + + storedFileInfo->mType = StructuredCloneFile::eBlob; + break; + } + + case StructuredCloneFile::eMutableFile: { + MOZ_ASSERT(file.type() == + DatabaseOrMutableFile::TPBackgroundMutableFileParent); + + auto mutableFileActor = + static_cast<MutableFile*>( + file.get_PBackgroundMutableFileParent()); + MOZ_ASSERT(mutableFileActor); + + storedFileInfo->mFileInfo = mutableFileActor->GetFileInfo(); + MOZ_ASSERT(storedFileInfo->mFileInfo); + + storedFileInfo->mType = StructuredCloneFile::eMutableFile; + break; + } + + case StructuredCloneFile::eWasmBytecode: + case StructuredCloneFile::eWasmCompiled: { + MOZ_ASSERT(file.type() == + DatabaseOrMutableFile::TPBackgroundIDBDatabaseFileParent); + + storedFileInfo->mFileActor = + static_cast<DatabaseFile*>( + file.get_PBackgroundIDBDatabaseFileParent()); + MOZ_ASSERT(storedFileInfo->mFileActor); + + storedFileInfo->mFileInfo = storedFileInfo->mFileActor->GetFileInfo(); + MOZ_ASSERT(storedFileInfo->mFileInfo); + + storedFileInfo->mType = fileAddInfo.type(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + } + + if (mDataOverThreshold) { + StoredFileInfo* storedFileInfo = mStoredFileInfos.AppendElement(fallible); + MOZ_ASSERT(storedFileInfo); + + RefPtr<FileManager> fileManager = + aTransaction->GetDatabase()->GetFileManager(); + MOZ_ASSERT(fileManager); + + storedFileInfo->mFileInfo = fileManager->GetNewFileInfo(); + + storedFileInfo->mInputStream = + new SCInputStream(mParams.cloneInfo().data().data); + + storedFileInfo->mType = StructuredCloneFile::eStructuredClone; + } + + return true; +} + +nsresult +ObjectStoreAddOrPutRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection->GetStorageConnection()); + + PROFILER_LABEL("IndexedDB", + "ObjectStoreAddOrPutRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + if (NS_WARN_IF(IndexedDatabaseManager::InLowDiskSpaceMode())) { + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool objectStoreHasIndexes; + rv = ObjectStoreHasIndexes(this, + aConnection, + mParams.objectStoreId(), + mObjectStoreMayHaveIndexes, + &objectStoreHasIndexes); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // This will be the final key we use. + Key& key = mResponse; + key = mParams.key(); + + const bool keyUnset = key.IsUnset(); + const int64_t osid = mParams.objectStoreId(); + + // First delete old index_data_values if we're overwriting something and we + // have indexes. + if (mOverwrite && !keyUnset && objectStoreHasIndexes) { + rv = RemoveOldIndexDataValues(aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // The "|| keyUnset" here is mostly a debugging tool. If a key isn't + // specified we should never have a collision and so it shouldn't matter + // if we allow overwrite or not. By not allowing overwrite we raise + // detectable errors rather than corrupting data. + DatabaseConnection::CachedStatement stmt; + if (!mOverwrite || keyUnset) { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT INTO object_data " + "(object_store_id, key, file_ids, data) " + "VALUES (:osid, :key, :file_ids, :data);"), + &stmt); + } else { + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "INSERT OR REPLACE INTO object_data " + "(object_store_id, key, file_ids, data) " + "VALUES (:osid, :key, :file_ids, :data);"), + &stmt); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), osid); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + const SerializedStructuredCloneWriteInfo& cloneInfo = mParams.cloneInfo(); + const JSStructuredCloneData& cloneData = cloneInfo.data().data; + size_t cloneDataSize = cloneData.Size(); + + MOZ_ASSERT(!keyUnset || mMetadata->mCommonMetadata.autoIncrement(), + "Should have key unless autoIncrement"); + + int64_t autoIncrementNum = 0; + + if (mMetadata->mCommonMetadata.autoIncrement()) { + if (keyUnset) { + autoIncrementNum = mMetadata->mNextAutoIncrementId; + + MOZ_ASSERT(autoIncrementNum > 0); + + if (autoIncrementNum > (1LL << 53)) { + return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + } + + key.SetFromInteger(autoIncrementNum); + } else if (key.IsFloat() && + key.ToFloat() >= mMetadata->mNextAutoIncrementId) { + autoIncrementNum = floor(key.ToFloat()); + } + + if (keyUnset && mMetadata->mCommonMetadata.keyPath().IsValid()) { + const SerializedStructuredCloneWriteInfo& cloneInfo = mParams.cloneInfo(); + MOZ_ASSERT(cloneInfo.offsetToKeyProp()); + MOZ_ASSERT(cloneDataSize > sizeof(uint64_t)); + MOZ_ASSERT(cloneInfo.offsetToKeyProp() <= + (cloneDataSize - sizeof(uint64_t))); + + // Special case where someone put an object into an autoIncrement'ing + // objectStore with no key in its keyPath set. We needed to figure out + // which row id we would get above before we could set that properly. + uint64_t keyPropValue = + ReinterpretDoubleAsUInt64(static_cast<double>(autoIncrementNum)); + + static const size_t keyPropSize = sizeof(uint64_t); + + char keyPropBuffer[keyPropSize]; + LittleEndian::writeUint64(keyPropBuffer, keyPropValue); + + auto iter = cloneData.Iter(); + DebugOnly<bool> result = + iter.AdvanceAcrossSegments(cloneData, cloneInfo.offsetToKeyProp()); + MOZ_ASSERT(result); + + for (uint32_t index = 0; index < keyPropSize; index++) { + char* keyPropPointer = iter.Data(); + *keyPropPointer = keyPropBuffer[index]; + + result = iter.AdvanceAcrossSegments(cloneData, 1); + MOZ_ASSERT(result); + } + } + } + + key.BindToStatement(stmt, NS_LITERAL_CSTRING("key")); + + if (mDataOverThreshold) { + // The data we store in the SQLite database is a (signed) 64-bit integer. + // The flags are left-shifted 32 bits so the max value is 0xFFFFFFFF. + // The file_ids index occupies the lower 32 bits and its max is 0xFFFFFFFF. + static const uint32_t kCompressedFlag = (1<<0); + + uint32_t flags = 0; + flags |= kCompressedFlag; + + uint32_t index = mStoredFileInfos.Length() - 1; + + int64_t data = (uint64_t(flags) << 32) | index; + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("data"), data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsCString flatCloneData; + flatCloneData.SetLength(cloneDataSize); + auto iter = cloneData.Iter(); + cloneData.ReadBytes(iter, flatCloneData.BeginWriting(), cloneDataSize); + + // Compress the bytes before adding into the database. + const char* uncompressed = flatCloneData.BeginReading(); + size_t uncompressedLength = cloneDataSize; + + size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength); + + UniqueFreePtr<char> compressed( + static_cast<char*>(malloc(compressedLength))); + if (NS_WARN_IF(!compressed)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + snappy::RawCompress(uncompressed, uncompressedLength, compressed.get(), + &compressedLength); + + uint8_t* dataBuffer = reinterpret_cast<uint8_t*>(compressed.release()); + size_t dataBufferLength = compressedLength; + + rv = stmt->BindAdoptedBlobByName(NS_LITERAL_CSTRING("data"), dataBuffer, + dataBufferLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (!mStoredFileInfos.IsEmpty()) { + // Moved outside the loop to allow it to be cached when demanded by the + // first write. (We may have mStoredFileInfos without any required writes.) + Maybe<FileHelper> fileHelper; + nsAutoString fileIds; + + for (uint32_t count = mStoredFileInfos.Length(), index = 0; + index < count; + index++) { + StoredFileInfo& storedFileInfo = mStoredFileInfos[index]; + MOZ_ASSERT(storedFileInfo.mFileInfo); + + // If there is a StoredFileInfo, then one of the following is true: + // - This was an overflow structured clone and storedFileInfo.mInputStream + // MUST be non-null. + // - This is a reference to a Blob that may or may not have already been + // written to disk. storedFileInfo.mFileActor MUST be non-null, but + // its GetBlockingInputStream may return null (so don't assert on them). + // - It's a mutable file. No writing will be performed. + MOZ_ASSERT(storedFileInfo.mInputStream || storedFileInfo.mFileActor || + storedFileInfo.mType == StructuredCloneFile::eMutableFile); + + nsCOMPtr<nsIInputStream> inputStream; + // Check for an explicit stream, like a structured clone stream. + storedFileInfo.mInputStream.swap(inputStream); + // Check for a blob-backed stream otherwise. + if (!inputStream && storedFileInfo.mFileActor) { + ErrorResult streamRv; + inputStream = + storedFileInfo.mFileActor->GetBlockingInputStream(streamRv); + if (NS_WARN_IF(streamRv.Failed())) { + return streamRv.StealNSResult(); + } + } + + if (inputStream) { + if (fileHelper.isNothing()) { + RefPtr<FileManager> fileManager = + Transaction()->GetDatabase()->GetFileManager(); + MOZ_ASSERT(fileManager); + + fileHelper.emplace(fileManager); + rv = fileHelper->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + RefPtr<FileInfo>& fileInfo = storedFileInfo.mFileInfo; + + nsCOMPtr<nsIFile> file = fileHelper->GetFile(fileInfo); + if (NS_WARN_IF(!file)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsCOMPtr<nsIFile> journalFile = + fileHelper->GetJournalFile(fileInfo); + if (NS_WARN_IF(!journalFile)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + bool compress = + storedFileInfo.mType == StructuredCloneFile::eStructuredClone; + + rv = fileHelper->CreateFileFromStream(file, + journalFile, + inputStream, + compress); + if (NS_FAILED(rv) && + NS_ERROR_GET_MODULE(rv) != NS_ERROR_MODULE_DOM_INDEXEDDB) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + // Try to remove the file if the copy failed. + nsresult rv2 = fileHelper->RemoveFile(file, journalFile); + if (NS_WARN_IF(NS_FAILED(rv2))) { + return rv; + } + return rv; + } + + if (storedFileInfo.mFileActor) { + storedFileInfo.mFileActor->WriteSucceededClearBlobImpl(); + } + } + + if (index) { + fileIds.Append(' '); + } + storedFileInfo.Serialize(fileIds); + } + + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("file_ids"), fileIds); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("file_ids")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = stmt->Execute(); + if (rv == NS_ERROR_STORAGE_CONSTRAINT) { + MOZ_ASSERT(!keyUnset, "Generated key had a collision!"); + return rv; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update our indexes if needed. + if (!mParams.indexUpdateInfos().IsEmpty()) { + MOZ_ASSERT(mUniqueIndexTable.isSome()); + + // Write the index_data_values column. + AutoTArray<IndexDataValue, 32> indexValues; + rv = IndexDataValuesFromUpdateInfos(mParams.indexUpdateInfos(), + mUniqueIndexTable.ref(), + indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = UpdateIndexValues(aConnection, osid, key, indexValues); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = InsertIndexTableRows(aConnection, osid, key, indexValues); + if (NS_FAILED(rv)) { + return rv; + } + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (autoIncrementNum) { + mMetadata->mNextAutoIncrementId = autoIncrementNum + 1; + Transaction()->NoteModifiedAutoIncrementObjectStore(mMetadata); + } + + return NS_OK; +} + +void +ObjectStoreAddOrPutRequestOp::GetResponse(RequestResponse& aResponse) +{ + AssertIsOnOwningThread(); + + if (mOverwrite) { + aResponse = ObjectStorePutResponse(mResponse); + } else { + aResponse = ObjectStoreAddResponse(mResponse); + } +} + +void +ObjectStoreAddOrPutRequestOp::Cleanup() +{ + AssertIsOnOwningThread(); + + mStoredFileInfos.Clear(); + + NormalTransactionOp::Cleanup(); +} + +NS_IMPL_ISUPPORTS(ObjectStoreAddOrPutRequestOp::SCInputStream, nsIInputStream) + +NS_IMETHODIMP +ObjectStoreAddOrPutRequestOp:: +SCInputStream::Close() +{ + return NS_OK; +} + +NS_IMETHODIMP +ObjectStoreAddOrPutRequestOp:: +SCInputStream::Available(uint64_t* _retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObjectStoreAddOrPutRequestOp:: +SCInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) +{ + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +ObjectStoreAddOrPutRequestOp:: +SCInputStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, + uint32_t aCount, + uint32_t* _retval) +{ + *_retval = 0; + + while (aCount) { + uint32_t count = std::min(uint32_t(mIter.RemainingInSegment()), aCount); + if (!count) { + // We've run out of data in the last segment. + break; + } + + uint32_t written; + nsresult rv = + aWriter(this, aClosure, mIter.Data(), *_retval, count, &written); + if (NS_WARN_IF(NS_FAILED(rv))) { + // InputStreams do not propagate errors to caller. + return NS_OK; + } + + // Writer should write what we asked it to write. + MOZ_ASSERT(written == count); + + *_retval += count; + aCount -= count; + + mIter.Advance(mData, count); + } + + return NS_OK; +} + +NS_IMETHODIMP +ObjectStoreAddOrPutRequestOp:: +SCInputStream::IsNonBlocking(bool* _retval) +{ + *_retval = false; + return NS_OK; +} + +ObjectStoreGetRequestOp::ObjectStoreGetRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll) + : NormalTransactionOp(aTransaction) + , mObjectStoreId(aGetAll ? + aParams.get_ObjectStoreGetAllParams().objectStoreId() : + aParams.get_ObjectStoreGetParams().objectStoreId()) + , mDatabase(aTransaction->GetDatabase()) + , mOptionalKeyRange(aGetAll ? + aParams.get_ObjectStoreGetAllParams() + .optionalKeyRange() : + OptionalKeyRange(aParams.get_ObjectStoreGetParams() + .keyRange())) + , mBackgroundParent(aTransaction->GetBackgroundParent()) + , mPreprocessInfoCount(0) + , mLimit(aGetAll ? aParams.get_ObjectStoreGetAllParams().limit() : 1) + , mGetAll(aGetAll) +{ + MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreGetParams || + aParams.type() == RequestParams::TObjectStoreGetAllParams); + MOZ_ASSERT(mObjectStoreId); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT_IF(!aGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); + MOZ_ASSERT(mBackgroundParent); +} + +template <typename T> +void MoveData(StructuredCloneReadInfo& aInfo, T& aResult); + +template <> +void +MoveData<SerializedStructuredCloneReadInfo>( + StructuredCloneReadInfo& aInfo, + SerializedStructuredCloneReadInfo& aResult) +{ + aResult.data().data = Move(aInfo.mData); + aResult.hasPreprocessInfo() = aInfo.mHasPreprocessInfo; +} + +template <> +void +MoveData<WasmModulePreprocessInfo>(StructuredCloneReadInfo& aInfo, + WasmModulePreprocessInfo& aResult) +{ +} + +template <bool aForPreprocess, typename T> +nsresult +ObjectStoreGetRequestOp::ConvertResponse(StructuredCloneReadInfo& aInfo, + T& aResult) +{ + MoveData(aInfo, aResult); + + FallibleTArray<SerializedStructuredCloneFile> serializedFiles; + nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, + mDatabase, + aInfo.mFiles, + aForPreprocess, + serializedFiles); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(aResult.files().IsEmpty()); + + aResult.files().SwapElements(serializedFiles); + + return NS_OK; +} + +nsresult +ObjectStoreGetRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(!mGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); + MOZ_ASSERT_IF(!mGetAll, mLimit == 1); + + PROFILER_LABEL("IndexedDB", + "ObjectStoreGetRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool hasKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + nsAutoCString keyRangeClause; + if (hasKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + NS_LITERAL_CSTRING("key"), + keyRangeClause); + } + + nsCString limitClause; + if (mLimit) { + limitClause.AssignLiteral(" LIMIT "); + limitClause.AppendInt(mLimit); + } + + nsCString query = + NS_LITERAL_CSTRING("SELECT file_ids, data " + "FROM object_data " + "WHERE object_store_id = :osid") + + keyRangeClause + + NS_LITERAL_CSTRING(" ORDER BY key ASC") + + limitClause; + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasKeyRange) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + StructuredCloneReadInfo* cloneInfo = mResponse.AppendElement(fallible); + if (NS_WARN_IF(!cloneInfo)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = GetStructuredCloneReadInfoFromStatement(stmt, 1, 0, + mDatabase->GetFileManager(), + cloneInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (cloneInfo->mHasPreprocessInfo) { + mPreprocessInfoCount++; + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +bool +ObjectStoreGetRequestOp::HasPreprocessInfo() +{ + return mPreprocessInfoCount > 0; +} + +nsresult +ObjectStoreGetRequestOp::GetPreprocessParams(PreprocessParams& aParams) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mResponse.IsEmpty()); + + if (mGetAll) { + aParams = ObjectStoreGetAllPreprocessParams(); + + FallibleTArray<WasmModulePreprocessInfo> falliblePreprocessInfos; + if (NS_WARN_IF(!falliblePreprocessInfos.SetLength(mPreprocessInfoCount, + fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t fallibleIndex = 0; + for (uint32_t count = mResponse.Length(), index = 0; + index < count; + index++) { + StructuredCloneReadInfo& info = mResponse[index]; + + if (info.mHasPreprocessInfo) { + nsresult rv = + ConvertResponse<true>(info, falliblePreprocessInfos[fallibleIndex++]); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + nsTArray<WasmModulePreprocessInfo>& preprocessInfos = + aParams.get_ObjectStoreGetAllPreprocessParams().preprocessInfos(); + + falliblePreprocessInfos.SwapElements(preprocessInfos); + + return NS_OK; + } + + aParams = ObjectStoreGetPreprocessParams(); + + WasmModulePreprocessInfo& preprocessInfo = + aParams.get_ObjectStoreGetPreprocessParams().preprocessInfo(); + + nsresult rv = ConvertResponse<true>(mResponse[0], preprocessInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +ObjectStoreGetRequestOp::GetResponse(RequestResponse& aResponse) +{ + MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); + + if (mGetAll) { + aResponse = ObjectStoreGetAllResponse(); + + if (!mResponse.IsEmpty()) { + FallibleTArray<SerializedStructuredCloneReadInfo> fallibleCloneInfos; + if (NS_WARN_IF(!fallibleCloneInfos.SetLength(mResponse.Length(), + fallible))) { + aResponse = NS_ERROR_OUT_OF_MEMORY; + return; + } + + for (uint32_t count = mResponse.Length(), index = 0; + index < count; + index++) { + nsresult rv = + ConvertResponse<false>(mResponse[index], fallibleCloneInfos[index]); + if (NS_WARN_IF(NS_FAILED(rv))) { + aResponse = rv; + return; + } + } + + nsTArray<SerializedStructuredCloneReadInfo>& cloneInfos = + aResponse.get_ObjectStoreGetAllResponse().cloneInfos(); + + fallibleCloneInfos.SwapElements(cloneInfos); + } + + return; + } + + aResponse = ObjectStoreGetResponse(); + + if (!mResponse.IsEmpty()) { + SerializedStructuredCloneReadInfo& serializedInfo = + aResponse.get_ObjectStoreGetResponse().cloneInfo(); + + nsresult rv = ConvertResponse<false>(mResponse[0], serializedInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + aResponse = rv; + } + } +} + +ObjectStoreGetKeyRequestOp::ObjectStoreGetKeyRequestOp( + TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll) + : NormalTransactionOp(aTransaction) + , mObjectStoreId(aGetAll ? + aParams.get_ObjectStoreGetAllKeysParams().objectStoreId() : + aParams.get_ObjectStoreGetKeyParams().objectStoreId()) + , mOptionalKeyRange(aGetAll ? + aParams.get_ObjectStoreGetAllKeysParams() + .optionalKeyRange() : + OptionalKeyRange(aParams.get_ObjectStoreGetKeyParams() + .keyRange())) + , mLimit(aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().limit() : 1) + , mGetAll(aGetAll) +{ + MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreGetKeyParams || + aParams.type() == RequestParams::TObjectStoreGetAllKeysParams); + MOZ_ASSERT(mObjectStoreId); + MOZ_ASSERT_IF(!aGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); +} + +nsresult +ObjectStoreGetKeyRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "ObjectStoreGetKeyRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool hasKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + nsAutoCString keyRangeClause; + if (hasKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + NS_LITERAL_CSTRING("key"), + keyRangeClause); + } + + nsAutoCString limitClause; + if (mLimit) { + limitClause.AssignLiteral(" LIMIT "); + limitClause.AppendInt(mLimit); + } + + nsCString query = + NS_LITERAL_CSTRING("SELECT key " + "FROM object_data " + "WHERE object_store_id = :osid") + + keyRangeClause + + NS_LITERAL_CSTRING(" ORDER BY key ASC") + + limitClause; + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasKeyRange) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + while(NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + Key* key = mResponse.AppendElement(fallible); + if (NS_WARN_IF(!key)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = key->SetFromStatement(stmt, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +void +ObjectStoreGetKeyRequestOp::GetResponse(RequestResponse& aResponse) +{ + MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); + + if (mGetAll) { + aResponse = ObjectStoreGetAllKeysResponse(); + + if (!mResponse.IsEmpty()) { + nsTArray<Key>& response = + aResponse.get_ObjectStoreGetAllKeysResponse().keys(); + mResponse.SwapElements(response); + } + + return; + } + + aResponse = ObjectStoreGetKeyResponse(); + + if (!mResponse.IsEmpty()) { + aResponse.get_ObjectStoreGetKeyResponse().key() = Move(mResponse[0]); + } +} + +ObjectStoreDeleteRequestOp::ObjectStoreDeleteRequestOp( + TransactionBase* aTransaction, + const ObjectStoreDeleteParams& aParams) + : NormalTransactionOp(aTransaction) + , mParams(aParams) + , mObjectStoreMayHaveIndexes(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + + RefPtr<FullObjectStoreMetadata> metadata = + aTransaction->GetMetadataForObjectStoreId(mParams.objectStoreId()); + MOZ_ASSERT(metadata); + + mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); +} + +nsresult +ObjectStoreDeleteRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + PROFILER_LABEL("IndexedDB", + "ObjectStoreDeleteRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool objectStoreHasIndexes; + rv = ObjectStoreHasIndexes(this, + aConnection, + mParams.objectStoreId(), + mObjectStoreMayHaveIndexes, + &objectStoreHasIndexes); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (objectStoreHasIndexes) { + rv = DeleteObjectStoreDataTableRowsWithIndexes(aConnection, + mParams.objectStoreId(), + mParams.keyRange()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + NS_NAMED_LITERAL_CSTRING(objectStoreIdString, "object_store_id"); + + nsAutoCString keyRangeClause; + GetBindingClauseForKeyRange(mParams.keyRange(), + NS_LITERAL_CSTRING("key"), + keyRangeClause); + + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement( + NS_LITERAL_CSTRING("DELETE FROM object_data " + "WHERE object_store_id = :") + objectStoreIdString + + keyRangeClause + + NS_LITERAL_CSTRING(";"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(objectStoreIdString, mParams.objectStoreId()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = BindKeyRangeToStatement(mParams.keyRange(), stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +ObjectStoreClearRequestOp::ObjectStoreClearRequestOp( + TransactionBase* aTransaction, + const ObjectStoreClearParams& aParams) + : NormalTransactionOp(aTransaction) + , mParams(aParams) + , mObjectStoreMayHaveIndexes(false) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + + RefPtr<FullObjectStoreMetadata> metadata = + aTransaction->GetMetadataForObjectStoreId(mParams.objectStoreId()); + MOZ_ASSERT(metadata); + + mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); +} + +nsresult +ObjectStoreClearRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "ObjectStoreClearRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + DatabaseConnection::AutoSavepoint autoSave; + nsresult rv = autoSave.Start(Transaction()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool objectStoreHasIndexes; + rv = ObjectStoreHasIndexes(this, + aConnection, + mParams.objectStoreId(), + mObjectStoreMayHaveIndexes, + &objectStoreHasIndexes); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (objectStoreHasIndexes) { + rv = DeleteObjectStoreDataTableRowsWithIndexes(aConnection, + mParams.objectStoreId(), + void_t()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + DatabaseConnection::CachedStatement stmt; + rv = aConnection->GetCachedStatement(NS_LITERAL_CSTRING( + "DELETE FROM object_data " + "WHERE object_store_id = :object_store_id;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("object_store_id"), + mParams.objectStoreId()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = autoSave.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +ObjectStoreCountRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "ObjectStoreCountRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool hasKeyRange = + mParams.optionalKeyRange().type() == OptionalKeyRange::TSerializedKeyRange; + + nsAutoCString keyRangeClause; + if (hasKeyRange) { + GetBindingClauseForKeyRange( + mParams.optionalKeyRange().get_SerializedKeyRange(), + NS_LITERAL_CSTRING("key"), + keyRangeClause); + } + + nsCString query = + NS_LITERAL_CSTRING("SELECT count(*) " + "FROM object_data " + "WHERE object_store_id = :osid") + + keyRangeClause; + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"), + mParams.objectStoreId()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasKeyRange) { + rv = BindKeyRangeToStatement( + mParams.optionalKeyRange().get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasResult)) { + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + int64_t count = stmt->AsInt64(0); + if (NS_WARN_IF(count < 0)) { + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mResponse.count() = count; + + return NS_OK; +} + +// static +already_AddRefed<FullIndexMetadata> +IndexRequestOpBase::IndexMetadataForParams(TransactionBase* aTransaction, + const RequestParams& aParams) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransaction); + MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetParams || + aParams.type() == RequestParams::TIndexGetKeyParams || + aParams.type() == RequestParams::TIndexGetAllParams || + aParams.type() == RequestParams::TIndexGetAllKeysParams || + aParams.type() == RequestParams::TIndexCountParams); + + uint64_t objectStoreId; + uint64_t indexId; + + switch (aParams.type()) { + case RequestParams::TIndexGetParams: { + const IndexGetParams& params = aParams.get_IndexGetParams(); + objectStoreId = params.objectStoreId(); + indexId = params.indexId(); + break; + } + + case RequestParams::TIndexGetKeyParams: { + const IndexGetKeyParams& params = aParams.get_IndexGetKeyParams(); + objectStoreId = params.objectStoreId(); + indexId = params.indexId(); + break; + } + + case RequestParams::TIndexGetAllParams: { + const IndexGetAllParams& params = aParams.get_IndexGetAllParams(); + objectStoreId = params.objectStoreId(); + indexId = params.indexId(); + break; + } + + case RequestParams::TIndexGetAllKeysParams: { + const IndexGetAllKeysParams& params = aParams.get_IndexGetAllKeysParams(); + objectStoreId = params.objectStoreId(); + indexId = params.indexId(); + break; + } + + case RequestParams::TIndexCountParams: { + const IndexCountParams& params = aParams.get_IndexCountParams(); + objectStoreId = params.objectStoreId(); + indexId = params.indexId(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + const RefPtr<FullObjectStoreMetadata> objectStoreMetadata = + aTransaction->GetMetadataForObjectStoreId(objectStoreId); + MOZ_ASSERT(objectStoreMetadata); + + RefPtr<FullIndexMetadata> indexMetadata = + aTransaction->GetMetadataForIndexId(objectStoreMetadata, indexId); + MOZ_ASSERT(indexMetadata); + + return indexMetadata.forget(); +} + +IndexGetRequestOp::IndexGetRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll) + : IndexRequestOpBase(aTransaction, aParams) + , mDatabase(aTransaction->GetDatabase()) + , mOptionalKeyRange(aGetAll ? + aParams.get_IndexGetAllParams().optionalKeyRange() : + OptionalKeyRange(aParams.get_IndexGetParams() + .keyRange())) + , mBackgroundParent(aTransaction->GetBackgroundParent()) + , mLimit(aGetAll ? aParams.get_IndexGetAllParams().limit() : 1) + , mGetAll(aGetAll) +{ + MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetParams || + aParams.type() == RequestParams::TIndexGetAllParams); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT_IF(!aGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); + MOZ_ASSERT(mBackgroundParent); +} + +nsresult +IndexGetRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(!mGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); + MOZ_ASSERT_IF(!mGetAll, mLimit == 1); + + PROFILER_LABEL("IndexedDB", + "IndexGetRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool hasKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + nsCString indexTable; + if (mMetadata->mCommonMetadata.unique()) { + indexTable.AssignLiteral("unique_index_data "); + } + else { + indexTable.AssignLiteral("index_data "); + } + + nsAutoCString keyRangeClause; + if (hasKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + NS_LITERAL_CSTRING("value"), + keyRangeClause); + } + + nsCString limitClause; + if (mLimit) { + limitClause.AssignLiteral(" LIMIT "); + limitClause.AppendInt(mLimit); + } + + nsCString query = + NS_LITERAL_CSTRING("SELECT file_ids, data " + "FROM object_data " + "INNER JOIN ") + + indexTable + + NS_LITERAL_CSTRING("AS index_table " + "ON object_data.object_store_id = " + "index_table.object_store_id " + "AND object_data.key = " + "index_table.object_data_key " + "WHERE index_id = :index_id") + + keyRangeClause + + limitClause; + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), + mMetadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasKeyRange) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + while(NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + StructuredCloneReadInfo* cloneInfo = mResponse.AppendElement(fallible); + if (NS_WARN_IF(!cloneInfo)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = GetStructuredCloneReadInfoFromStatement(stmt, 1, 0, + mDatabase->GetFileManager(), + cloneInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (cloneInfo->mHasPreprocessInfo) { + IDB_WARNING("Preprocessing for indexes not yet implemented!"); + return NS_ERROR_NOT_IMPLEMENTED; + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +void +IndexGetRequestOp::GetResponse(RequestResponse& aResponse) +{ + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + if (mGetAll) { + aResponse = IndexGetAllResponse(); + + if (!mResponse.IsEmpty()) { + FallibleTArray<SerializedStructuredCloneReadInfo> fallibleCloneInfos; + if (NS_WARN_IF(!fallibleCloneInfos.SetLength(mResponse.Length(), + fallible))) { + aResponse = NS_ERROR_OUT_OF_MEMORY; + return; + } + + for (uint32_t count = mResponse.Length(), index = 0; + index < count; + index++) { + StructuredCloneReadInfo& info = mResponse[index]; + + SerializedStructuredCloneReadInfo& serializedInfo = + fallibleCloneInfos[index]; + + serializedInfo.data().data = Move(info.mData); + + FallibleTArray<SerializedStructuredCloneFile> serializedFiles; + nsresult rv = SerializeStructuredCloneFiles(mBackgroundParent, + mDatabase, + info.mFiles, + /* aForPreprocess */ false, + serializedFiles); + if (NS_WARN_IF(NS_FAILED(rv))) { + aResponse = rv; + return; + } + + MOZ_ASSERT(serializedInfo.files().IsEmpty()); + + serializedInfo.files().SwapElements(serializedFiles); + } + + nsTArray<SerializedStructuredCloneReadInfo>& cloneInfos = + aResponse.get_IndexGetAllResponse().cloneInfos(); + + fallibleCloneInfos.SwapElements(cloneInfos); + } + + return; + } + + aResponse = IndexGetResponse(); + + if (!mResponse.IsEmpty()) { + StructuredCloneReadInfo& info = mResponse[0]; + + SerializedStructuredCloneReadInfo& serializedInfo = + aResponse.get_IndexGetResponse().cloneInfo(); + + serializedInfo.data().data = Move(info.mData); + + FallibleTArray<SerializedStructuredCloneFile> serializedFiles; + nsresult rv = + SerializeStructuredCloneFiles(mBackgroundParent, + mDatabase, + info.mFiles, + /* aForPreprocess */ false, + serializedFiles); + if (NS_WARN_IF(NS_FAILED(rv))) { + aResponse = rv; + return; + } + + MOZ_ASSERT(serializedInfo.files().IsEmpty()); + + serializedInfo.files().SwapElements(serializedFiles); + } +} + +IndexGetKeyRequestOp::IndexGetKeyRequestOp(TransactionBase* aTransaction, + const RequestParams& aParams, + bool aGetAll) + : IndexRequestOpBase(aTransaction, aParams) + , mOptionalKeyRange(aGetAll ? + aParams.get_IndexGetAllKeysParams().optionalKeyRange() : + OptionalKeyRange(aParams.get_IndexGetKeyParams() + .keyRange())) + , mLimit(aGetAll ? aParams.get_IndexGetAllKeysParams().limit() : 1) + , mGetAll(aGetAll) +{ + MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetKeyParams || + aParams.type() == RequestParams::TIndexGetAllKeysParams); + MOZ_ASSERT_IF(!aGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); +} + +nsresult +IndexGetKeyRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(!mGetAll, + mOptionalKeyRange.type() == + OptionalKeyRange::TSerializedKeyRange); + MOZ_ASSERT_IF(!mGetAll, mLimit == 1); + + PROFILER_LABEL("IndexedDB", + "IndexGetKeyRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool hasKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + nsCString indexTable; + if (mMetadata->mCommonMetadata.unique()) { + indexTable.AssignLiteral("unique_index_data "); + } + else { + indexTable.AssignLiteral("index_data "); + } + + nsAutoCString keyRangeClause; + if (hasKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + NS_LITERAL_CSTRING("value"), + keyRangeClause); + } + + nsCString limitClause; + if (mLimit) { + limitClause.AssignLiteral(" LIMIT "); + limitClause.AppendInt(mLimit); + } + + nsCString query = + NS_LITERAL_CSTRING("SELECT object_data_key " + "FROM ") + + indexTable + + NS_LITERAL_CSTRING("WHERE index_id = :index_id") + + keyRangeClause + + limitClause; + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), + mMetadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasKeyRange) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + while(NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { + Key* key = mResponse.AppendElement(fallible); + if (NS_WARN_IF(!key)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + rv = key->SetFromStatement(stmt, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +void +IndexGetKeyRequestOp::GetResponse(RequestResponse& aResponse) +{ + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + if (mGetAll) { + aResponse = IndexGetAllKeysResponse(); + + if (!mResponse.IsEmpty()) { + mResponse.SwapElements(aResponse.get_IndexGetAllKeysResponse().keys()); + } + + return; + } + + aResponse = IndexGetKeyResponse(); + + if (!mResponse.IsEmpty()) { + aResponse.get_IndexGetKeyResponse().key() = Move(mResponse[0]); + } +} + +nsresult +IndexCountRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + PROFILER_LABEL("IndexedDB", + "IndexCountRequestOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool hasKeyRange = + mParams.optionalKeyRange().type() == OptionalKeyRange::TSerializedKeyRange; + + nsCString indexTable; + if (mMetadata->mCommonMetadata.unique()) { + indexTable.AssignLiteral("unique_index_data "); + } + else { + indexTable.AssignLiteral("index_data "); + } + + nsAutoCString keyRangeClause; + if (hasKeyRange) { + GetBindingClauseForKeyRange( + mParams.optionalKeyRange().get_SerializedKeyRange(), + NS_LITERAL_CSTRING("value"), + keyRangeClause); + } + + nsCString query = + NS_LITERAL_CSTRING("SELECT count(*) " + "FROM ") + + indexTable + + NS_LITERAL_CSTRING("WHERE index_id = :index_id") + + keyRangeClause; + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"), + mMetadata->mCommonMetadata.id()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (hasKeyRange) { + rv = BindKeyRangeToStatement( + mParams.optionalKeyRange().get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!hasResult)) { + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + int64_t count = stmt->AsInt64(0); + if (NS_WARN_IF(count < 0)) { + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mResponse.count() = count; + + return NS_OK; +} + +bool +Cursor:: +CursorOpBase::SendFailureResult(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); + MOZ_ASSERT(!mResponseSent); + + if (!IsActorDestroyed()) { + mResponse = ClampResultCode(aResultCode); + + // This is an expected race when the transaction is invalidated after + // data is retrieved from database. We clear the retrieved files to prevent + // the assertion failure in SendResponseInternal when mResponse.type() is + // CursorResponse::Tnsresult. + if (Transaction()->IsInvalidated() && !mFiles.IsEmpty()) { + mFiles.Clear(); + } + + mCursor->SendResponseInternal(mResponse, mFiles); + } + +#ifdef DEBUG + mResponseSent = true; +#endif + return false; +} + +void +Cursor:: +CursorOpBase::Cleanup() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT_IF(!IsActorDestroyed(), mResponseSent); + + mCursor = nullptr; + +#ifdef DEBUG + // A bit hacky but the CursorOp request is not generated in response to a + // child request like most other database operations. Do this to make our + // assertions happy. + NoteActorDestroyed(); +#endif + + TransactionDatabaseOperationBase::Cleanup(); +} + +nsresult +Cursor:: +CursorOpBase::PopulateResponseFromStatement( + DatabaseConnection::CachedStatement& aStmt, + bool aInitializeResponse) +{ + Transaction()->AssertIsOnConnectionThread(); + MOZ_ASSERT(mResponse.type() == CursorResponse::T__None); + MOZ_ASSERT_IF(mFiles.IsEmpty(), aInitializeResponse); + + nsresult rv = mCursor->mKey.SetFromStatement(aStmt, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + switch (mCursor->mType) { + case OpenCursorParams::TObjectStoreOpenCursorParams: { + StructuredCloneReadInfo cloneInfo; + rv = GetStructuredCloneReadInfoFromStatement(aStmt, + 2, + 1, + mCursor->mFileManager, + &cloneInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (cloneInfo.mHasPreprocessInfo) { + IDB_WARNING("Preprocessing for cursors not yet implemented!"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + if (aInitializeResponse) { + mResponse = nsTArray<ObjectStoreCursorResponse>(); + } else { + MOZ_ASSERT(mResponse.type() == + CursorResponse::TArrayOfObjectStoreCursorResponse); + } + + auto& responses = mResponse.get_ArrayOfObjectStoreCursorResponse(); + auto& response = *responses.AppendElement(); + response.cloneInfo().data().data = Move(cloneInfo.mData); + response.key() = mCursor->mKey; + + mFiles.AppendElement(Move(cloneInfo.mFiles)); + break; + } + + case OpenCursorParams::TObjectStoreOpenKeyCursorParams: { + MOZ_ASSERT(aInitializeResponse); + mResponse = ObjectStoreKeyCursorResponse(mCursor->mKey); + break; + } + + case OpenCursorParams::TIndexOpenCursorParams: { + rv = mCursor->mSortKey.SetFromStatement(aStmt, 1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mCursor->mObjectKey.SetFromStatement(aStmt, 2); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + StructuredCloneReadInfo cloneInfo; + rv = GetStructuredCloneReadInfoFromStatement(aStmt, + 4, + 3, + mCursor->mFileManager, + &cloneInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (cloneInfo.mHasPreprocessInfo) { + IDB_WARNING("Preprocessing for cursors not yet implemented!"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + MOZ_ASSERT(aInitializeResponse); + mResponse = IndexCursorResponse(); + + auto& response = mResponse.get_IndexCursorResponse(); + response.cloneInfo().data().data = Move(cloneInfo.mData); + response.key() = mCursor->mKey; + response.sortKey() = mCursor->mSortKey; + response.objectKey() = mCursor->mObjectKey; + + mFiles.AppendElement(Move(cloneInfo.mFiles)); + break; + } + + case OpenCursorParams::TIndexOpenKeyCursorParams: { + rv = mCursor->mSortKey.SetFromStatement(aStmt, 1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mCursor->mObjectKey.SetFromStatement(aStmt, 2); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(aInitializeResponse); + mResponse = IndexKeyCursorResponse(mCursor->mKey, + mCursor->mSortKey, + mCursor->mObjectKey); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return NS_OK; +} + +void +Cursor:: +OpenOp::GetRangeKeyInfo(bool aLowerBound, Key* aKey, bool* aOpen) +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(aKey); + MOZ_ASSERT(aKey->IsUnset()); + MOZ_ASSERT(aOpen); + + if (mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange) { + const SerializedKeyRange& range = + mOptionalKeyRange.get_SerializedKeyRange(); + if (range.isOnly()) { + *aKey = range.lower(); + *aOpen = false; +#ifdef ENABLE_INTL_API + if (mCursor->IsLocaleAware()) { + range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale); + } +#endif + } else { + *aKey = aLowerBound ? range.lower() : range.upper(); + *aOpen = aLowerBound ? range.lowerOpen() : range.upperOpen(); +#ifdef ENABLE_INTL_API + if (mCursor->IsLocaleAware()) { + if (aLowerBound) { + range.lower().ToLocaleBasedKey(*aKey, mCursor->mLocale); + } else { + range.upper().ToLocaleBasedKey(*aKey, mCursor->mLocale); + } + } +#endif + } + } else { + *aOpen = false; + } +} + +nsresult +Cursor:: +OpenOp::DoObjectStoreDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mType == OpenCursorParams::TObjectStoreOpenCursorParams); + MOZ_ASSERT(mCursor->mObjectStoreId); + MOZ_ASSERT(mCursor->mFileManager); + + PROFILER_LABEL("IndexedDB", + "Cursor::OpenOp::DoObjectStoreDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool usingKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + NS_NAMED_LITERAL_CSTRING(keyString, "key"); + NS_NAMED_LITERAL_CSTRING(id, "id"); + NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); + + nsCString queryStart = + NS_LITERAL_CSTRING("SELECT ") + + keyString + + NS_LITERAL_CSTRING(", file_ids, data " + "FROM object_data " + "WHERE object_store_id = :") + + id; + + nsAutoCString keyRangeClause; + if (usingKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + keyString, + keyRangeClause); + } + + nsAutoCString directionClause = NS_LITERAL_CSTRING(" ORDER BY ") + keyString; + switch (mCursor->mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: + directionClause.AppendLiteral(" ASC"); + break; + + case IDBCursor::PREV: + case IDBCursor::PREV_UNIQUE: + directionClause.AppendLiteral(" DESC"); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + nsCString firstQuery = + queryStart + + keyRangeClause + + directionClause + + openLimit + + NS_LITERAL_CSTRING("1"); + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(id, mCursor->mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (usingKeyRange) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!hasResult) { + mResponse = void_t(); + return NS_OK; + } + + rv = PopulateResponseFromStatement(stmt, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now we need to make the query to get the next match. + keyRangeClause.Truncate(); + nsAutoCString continueToKeyRangeClause; + + NS_NAMED_LITERAL_CSTRING(currentKey, "current_key"); + NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); + + switch (mCursor->mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: { + Key upper; + bool open; + GetRangeKeyInfo(false, &upper, &open); + AppendConditionClause(keyString, currentKey, false, false, + keyRangeClause); + AppendConditionClause(keyString, currentKey, false, true, + continueToKeyRangeClause); + if (usingKeyRange && !upper.IsUnset()) { + AppendConditionClause(keyString, rangeKey, true, !open, keyRangeClause); + AppendConditionClause(keyString, rangeKey, true, !open, + continueToKeyRangeClause); + mCursor->mRangeKey = upper; + } + break; + } + + case IDBCursor::PREV: + case IDBCursor::PREV_UNIQUE: { + Key lower; + bool open; + GetRangeKeyInfo(true, &lower, &open); + AppendConditionClause(keyString, currentKey, true, false, keyRangeClause); + AppendConditionClause(keyString, currentKey, true, true, + continueToKeyRangeClause); + if (usingKeyRange && !lower.IsUnset()) { + AppendConditionClause(keyString, rangeKey, false, !open, + keyRangeClause); + AppendConditionClause(keyString, rangeKey, false, !open, + continueToKeyRangeClause); + mCursor->mRangeKey = lower; + } + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + mCursor->mContinueQuery = + queryStart + + keyRangeClause + + directionClause + + openLimit; + + mCursor->mContinueToQuery = + queryStart + + continueToKeyRangeClause + + directionClause + + openLimit; + + return NS_OK; +} + +nsresult +Cursor:: +OpenOp::DoObjectStoreKeyDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mType == + OpenCursorParams::TObjectStoreOpenKeyCursorParams); + MOZ_ASSERT(mCursor->mObjectStoreId); + + PROFILER_LABEL("IndexedDB", + "Cursor::OpenOp::DoObjectStoreKeyDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool usingKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + NS_NAMED_LITERAL_CSTRING(keyString, "key"); + NS_NAMED_LITERAL_CSTRING(id, "id"); + NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); + + nsCString queryStart = + NS_LITERAL_CSTRING("SELECT ") + + keyString + + NS_LITERAL_CSTRING(" FROM object_data " + "WHERE object_store_id = :") + + id; + + nsAutoCString keyRangeClause; + if (usingKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + keyString, + keyRangeClause); + } + + nsAutoCString directionClause = NS_LITERAL_CSTRING(" ORDER BY ") + keyString; + switch (mCursor->mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: + directionClause.AppendLiteral(" ASC"); + break; + + case IDBCursor::PREV: + case IDBCursor::PREV_UNIQUE: + directionClause.AppendLiteral(" DESC"); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + nsCString firstQuery = + queryStart + + keyRangeClause + + directionClause + + openLimit + + NS_LITERAL_CSTRING("1"); + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(id, mCursor->mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (usingKeyRange) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!hasResult) { + mResponse = void_t(); + return NS_OK; + } + + rv = PopulateResponseFromStatement(stmt, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now we need to make the query to get the next match. + keyRangeClause.Truncate(); + nsAutoCString continueToKeyRangeClause; + + NS_NAMED_LITERAL_CSTRING(currentKey, "current_key"); + NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); + + switch (mCursor->mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: { + Key upper; + bool open; + GetRangeKeyInfo(false, &upper, &open); + AppendConditionClause(keyString, currentKey, false, false, + keyRangeClause); + AppendConditionClause(keyString, currentKey, false, true, + continueToKeyRangeClause); + if (usingKeyRange && !upper.IsUnset()) { + AppendConditionClause(keyString, rangeKey, true, !open, keyRangeClause); + AppendConditionClause(keyString, rangeKey, true, !open, + continueToKeyRangeClause); + mCursor->mRangeKey = upper; + } + break; + } + + case IDBCursor::PREV: + case IDBCursor::PREV_UNIQUE: { + Key lower; + bool open; + GetRangeKeyInfo(true, &lower, &open); + AppendConditionClause(keyString, currentKey, true, false, keyRangeClause); + AppendConditionClause(keyString, currentKey, true, true, + continueToKeyRangeClause); + if (usingKeyRange && !lower.IsUnset()) { + AppendConditionClause(keyString, rangeKey, false, !open, + keyRangeClause); + AppendConditionClause(keyString, rangeKey, false, !open, + continueToKeyRangeClause); + mCursor->mRangeKey = lower; + } + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + mCursor->mContinueQuery = + queryStart + + keyRangeClause + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + continueToKeyRangeClause + + directionClause + + openLimit; + + return NS_OK; +} + +nsresult +Cursor:: +OpenOp::DoIndexDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mType == OpenCursorParams::TIndexOpenCursorParams); + MOZ_ASSERT(mCursor->mObjectStoreId); + MOZ_ASSERT(mCursor->mIndexId); + + PROFILER_LABEL("IndexedDB", + "Cursor::OpenOp::DoIndexDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool usingKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + nsCString indexTable = mCursor->mUniqueIndex ? + NS_LITERAL_CSTRING("unique_index_data") : + NS_LITERAL_CSTRING("index_data"); + + NS_NAMED_LITERAL_CSTRING(sortColumn, "sort_column"); + NS_NAMED_LITERAL_CSTRING(id, "id"); + NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); + + nsAutoCString sortColumnAlias; + if (mCursor->IsLocaleAware()) { + sortColumnAlias = "SELECT index_table.value, " + "index_table.value_locale as sort_column, "; + } else { + sortColumnAlias = "SELECT index_table.value as sort_column, " + "index_table.value_locale, "; + } + + nsAutoCString queryStart = + sortColumnAlias + + NS_LITERAL_CSTRING( "index_table.object_data_key, " + "object_data.file_ids, " + "object_data.data " + "FROM ") + + indexTable + + NS_LITERAL_CSTRING(" AS index_table " + "JOIN object_data " + "ON index_table.object_store_id = " + "object_data.object_store_id " + "AND index_table.object_data_key = " + "object_data.key " + "WHERE index_table.index_id = :") + + id; + + nsAutoCString keyRangeClause; + if (usingKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + sortColumn, + keyRangeClause); + } + + nsAutoCString directionClause = + NS_LITERAL_CSTRING(" ORDER BY ") + + sortColumn; + + switch (mCursor->mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: + directionClause.AppendLiteral(" ASC, index_table.object_data_key ASC"); + break; + + case IDBCursor::PREV: + directionClause.AppendLiteral(" DESC, index_table.object_data_key DESC"); + break; + + case IDBCursor::PREV_UNIQUE: + directionClause.AppendLiteral(" DESC, index_table.object_data_key ASC"); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + nsCString firstQuery = + queryStart + + keyRangeClause + + directionClause + + openLimit + + NS_LITERAL_CSTRING("1"); + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(id, mCursor->mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (usingKeyRange) { + if (mCursor->IsLocaleAware()) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt, + mCursor->mLocale); + } else { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!hasResult) { + mResponse = void_t(); + return NS_OK; + } + + rv = PopulateResponseFromStatement(stmt, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now we need to make the query to get the next match. + NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); + + switch (mCursor->mDirection) { + case IDBCursor::NEXT: { + Key upper; + bool open; + GetRangeKeyInfo(false, &upper, &open); + if (usingKeyRange && !upper.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); + mCursor->mRangeKey = upper; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " + "AND ( sort_column > :current_key OR " + "index_table.object_data_key > :object_key ) " + ) + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key") + + directionClause + + openLimit; + mCursor->mContinuePrimaryKeyQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " + "AND index_table.object_data_key >= :object_key " + ) + + directionClause + + openLimit; + break; + } + + case IDBCursor::NEXT_UNIQUE: { + Key upper; + bool open; + GetRangeKeyInfo(false, &upper, &open); + if (usingKeyRange && !upper.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); + mCursor->mRangeKey = upper; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column > :current_key") + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key") + + directionClause + + openLimit; + break; + } + + case IDBCursor::PREV: { + Key lower; + bool open; + GetRangeKeyInfo(true, &lower, &open); + if (usingKeyRange && !lower.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); + mCursor->mRangeKey = lower; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " + "AND ( sort_column < :current_key OR " + "index_table.object_data_key < :object_key ) " + ) + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key") + + directionClause + + openLimit; + mCursor->mContinuePrimaryKeyQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " + "AND index_table.object_data_key <= :object_key " + ) + + directionClause + + openLimit; + break; + } + + case IDBCursor::PREV_UNIQUE: { + Key lower; + bool open; + GetRangeKeyInfo(true, &lower, &open); + if (usingKeyRange && !lower.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); + mCursor->mRangeKey = lower; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column < :current_key") + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key") + + directionClause + + openLimit; + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return NS_OK; +} + +nsresult +Cursor:: +OpenOp::DoIndexKeyDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mType == OpenCursorParams::TIndexOpenKeyCursorParams); + MOZ_ASSERT(mCursor->mObjectStoreId); + MOZ_ASSERT(mCursor->mIndexId); + + PROFILER_LABEL("IndexedDB", + "Cursor::OpenOp::DoIndexKeyDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + const bool usingKeyRange = + mOptionalKeyRange.type() == OptionalKeyRange::TSerializedKeyRange; + + nsCString table = mCursor->mUniqueIndex ? + NS_LITERAL_CSTRING("unique_index_data") : + NS_LITERAL_CSTRING("index_data"); + + NS_NAMED_LITERAL_CSTRING(sortColumn, "sort_column"); + NS_NAMED_LITERAL_CSTRING(id, "id"); + NS_NAMED_LITERAL_CSTRING(openLimit, " LIMIT "); + + nsAutoCString sortColumnAlias; + if (mCursor->IsLocaleAware()) { + sortColumnAlias = "SELECT value, " + "value_locale as sort_column, "; + } else { + sortColumnAlias = "SELECT value as sort_column, " + "value_locale, "; + } + + nsAutoCString queryStart = + sortColumnAlias + + NS_LITERAL_CSTRING( "object_data_key " + " FROM ") + + table + + NS_LITERAL_CSTRING(" WHERE index_id = :") + + id; + + nsAutoCString keyRangeClause; + if (usingKeyRange) { + GetBindingClauseForKeyRange(mOptionalKeyRange.get_SerializedKeyRange(), + sortColumn, + keyRangeClause); + } + + nsAutoCString directionClause = + NS_LITERAL_CSTRING(" ORDER BY ") + + sortColumn; + + switch (mCursor->mDirection) { + case IDBCursor::NEXT: + case IDBCursor::NEXT_UNIQUE: + directionClause.AppendLiteral(" ASC, object_data_key ASC"); + break; + + case IDBCursor::PREV: + directionClause.AppendLiteral(" DESC, object_data_key DESC"); + break; + + case IDBCursor::PREV_UNIQUE: + directionClause.AppendLiteral(" DESC, object_data_key ASC"); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + nsCString firstQuery = + queryStart + + keyRangeClause + + directionClause + + openLimit + + NS_LITERAL_CSTRING("1"); + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(firstQuery, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByName(id, mCursor->mIndexId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (usingKeyRange) { + if (mCursor->IsLocaleAware()) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt, + mCursor->mLocale); + } else { + rv = BindKeyRangeToStatement(mOptionalKeyRange.get_SerializedKeyRange(), + stmt); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!hasResult) { + mResponse = void_t(); + return NS_OK; + } + + rv = PopulateResponseFromStatement(stmt, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now we need to make the query to get the next match. + NS_NAMED_LITERAL_CSTRING(rangeKey, "range_key"); + + switch (mCursor->mDirection) { + case IDBCursor::NEXT: { + Key upper; + bool open; + GetRangeKeyInfo(false, &upper, &open); + if (usingKeyRange && !upper.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); + mCursor->mRangeKey = upper; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " + "AND ( sort_column > :current_key OR " + "object_data_key > :object_key )") + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key ") + + directionClause + + openLimit; + mCursor->mContinuePrimaryKeyQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key " + "AND object_data_key >= :object_key " + ) + + directionClause + + openLimit; + break; + } + + case IDBCursor::NEXT_UNIQUE: { + Key upper; + bool open; + GetRangeKeyInfo(false, &upper, &open); + if (usingKeyRange && !upper.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, true, !open, queryStart); + mCursor->mRangeKey = upper; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column > :current_key") + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column >= :current_key") + + directionClause + + openLimit; + break; + } + + case IDBCursor::PREV: { + Key lower; + bool open; + GetRangeKeyInfo(true, &lower, &open); + if (usingKeyRange && !lower.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); + mCursor->mRangeKey = lower; + } + + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " + "AND ( sort_column < :current_key OR " + "object_data_key < :object_key )") + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key ") + + directionClause + + openLimit; + mCursor->mContinuePrimaryKeyQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key " + "AND object_data_key <= :object_key " + ) + + directionClause + + openLimit; + break; + } + + case IDBCursor::PREV_UNIQUE: { + Key lower; + bool open; + GetRangeKeyInfo(true, &lower, &open); + if (usingKeyRange && !lower.IsUnset()) { + AppendConditionClause(sortColumn, rangeKey, false, !open, queryStart); + mCursor->mRangeKey = lower; + } + mCursor->mContinueQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column < :current_key") + + directionClause + + openLimit; + mCursor->mContinueToQuery = + queryStart + + NS_LITERAL_CSTRING(" AND sort_column <= :current_key") + + directionClause + + openLimit; + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return NS_OK; +} + +nsresult +Cursor:: +OpenOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mContinueQuery.IsEmpty()); + MOZ_ASSERT(mCursor->mContinueToQuery.IsEmpty()); + MOZ_ASSERT(mCursor->mContinuePrimaryKeyQuery.IsEmpty()); + MOZ_ASSERT(mCursor->mKey.IsUnset()); + MOZ_ASSERT(mCursor->mRangeKey.IsUnset()); + + PROFILER_LABEL("IndexedDB", + "Cursor::OpenOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + + switch (mCursor->mType) { + case OpenCursorParams::TObjectStoreOpenCursorParams: + rv = DoObjectStoreDatabaseWork(aConnection); + break; + + case OpenCursorParams::TObjectStoreOpenKeyCursorParams: + rv = DoObjectStoreKeyDatabaseWork(aConnection); + break; + + case OpenCursorParams::TIndexOpenCursorParams: + rv = DoIndexDatabaseWork(aConnection); + break; + + case OpenCursorParams::TIndexOpenKeyCursorParams: + rv = DoIndexKeyDatabaseWork(aConnection); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +Cursor:: +OpenOp::SendSuccessResult() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); + MOZ_ASSERT(mResponse.type() != CursorResponse::T__None); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mKey.IsUnset()); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mSortKey.IsUnset()); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mRangeKey.IsUnset()); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mObjectKey.IsUnset()); + + if (IsActorDestroyed()) { + return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } + + mCursor->SendResponseInternal(mResponse, mFiles); + +#ifdef DEBUG + mResponseSent = true; +#endif + return NS_OK; +} + +nsresult +Cursor:: +ContinueOp::DoDatabaseWork(DatabaseConnection* aConnection) +{ + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mObjectStoreId); + MOZ_ASSERT(!mCursor->mContinueQuery.IsEmpty()); + MOZ_ASSERT(!mCursor->mContinueToQuery.IsEmpty()); + MOZ_ASSERT(!mCursor->mKey.IsUnset()); + + const bool isIndex = + mCursor->mType == OpenCursorParams::TIndexOpenCursorParams || + mCursor->mType == OpenCursorParams::TIndexOpenKeyCursorParams; + + MOZ_ASSERT_IF(isIndex && + (mCursor->mDirection == IDBCursor::NEXT || + mCursor->mDirection == IDBCursor::PREV), + !mCursor->mContinuePrimaryKeyQuery.IsEmpty()); + MOZ_ASSERT_IF(isIndex, mCursor->mIndexId); + MOZ_ASSERT_IF(isIndex, !mCursor->mObjectKey.IsUnset()); + + PROFILER_LABEL("IndexedDB", + "Cursor::ContinueOp::DoDatabaseWork", + js::ProfileEntry::Category::STORAGE); + + // We need to pick a query based on whether or not a key was passed to the + // continue function. If not we'll grab the the next item in the database that + // is greater than (or less than, if we're running a PREV cursor) the current + // key. If a key was passed we'll grab the next item in the database that is + // greater than (or less than, if we're running a PREV cursor) or equal to the + // key that was specified. + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + bool hasContinueKey = false; + bool hasContinuePrimaryKey = false; + uint32_t advanceCount = 1; + Key& currentKey = mCursor->IsLocaleAware() ? mCursor->mSortKey : mCursor->mKey; + + switch (mParams.type()) { + case CursorRequestParams::TContinueParams: + if (!mParams.get_ContinueParams().key().IsUnset()) { + hasContinueKey = true; + currentKey = mParams.get_ContinueParams().key(); + } + break; + case CursorRequestParams::TContinuePrimaryKeyParams: + MOZ_ASSERT(!mParams.get_ContinuePrimaryKeyParams().key().IsUnset()); + MOZ_ASSERT(!mParams.get_ContinuePrimaryKeyParams().primaryKey().IsUnset()); + MOZ_ASSERT(mCursor->mDirection == IDBCursor::NEXT || + mCursor->mDirection == IDBCursor::PREV); + hasContinueKey = true; + hasContinuePrimaryKey = true; + currentKey = mParams.get_ContinuePrimaryKeyParams().key(); + break; + case CursorRequestParams::TAdvanceParams: + advanceCount = mParams.get_AdvanceParams().count(); + break; + default: + MOZ_CRASH("Should never get here!"); + } + + const nsCString& continueQuery = + hasContinuePrimaryKey ? mCursor->mContinuePrimaryKeyQuery : + hasContinueKey ? mCursor->mContinueToQuery : mCursor->mContinueQuery; + + MOZ_ASSERT(advanceCount > 0); + nsAutoCString countString; + countString.AppendInt(advanceCount); + + nsCString query = continueQuery + countString; + + NS_NAMED_LITERAL_CSTRING(currentKeyName, "current_key"); + NS_NAMED_LITERAL_CSTRING(rangeKeyName, "range_key"); + NS_NAMED_LITERAL_CSTRING(objectKeyName, "object_key"); + + const bool usingRangeKey = !mCursor->mRangeKey.IsUnset(); + + DatabaseConnection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement(query, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t id = isIndex ? mCursor->mIndexId : mCursor->mObjectStoreId; + + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Bind current key. + rv = currentKey.BindToStatement(stmt, currentKeyName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Bind range key if it is specified. + if (usingRangeKey) { + rv = mCursor->mRangeKey.BindToStatement(stmt, rangeKeyName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Bind object key if duplicates are allowed and we're not continuing to a + // specific key. + if (isIndex && + !hasContinueKey && + (mCursor->mDirection == IDBCursor::NEXT || + mCursor->mDirection == IDBCursor::PREV)) { + rv = mCursor->mObjectKey.BindToStatement(stmt, objectKeyName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Bind object key if primaryKey is specified. + if (hasContinuePrimaryKey) { + rv = mParams.get_ContinuePrimaryKeyParams().primaryKey() + .BindToStatement(stmt, objectKeyName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + + bool hasResult; + for (uint32_t index = 0; index < advanceCount; index++) { + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!hasResult) { + mCursor->mKey.Unset(); + mCursor->mSortKey.Unset(); + mCursor->mRangeKey.Unset(); + mCursor->mObjectKey.Unset(); + mResponse = void_t(); + return NS_OK; + } + } + + rv = PopulateResponseFromStatement(stmt, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +Cursor:: +ContinueOp::SendSuccessResult() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mKey.IsUnset()); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mRangeKey.IsUnset()); + MOZ_ASSERT_IF(mResponse.type() == CursorResponse::Tvoid_t, + mCursor->mObjectKey.IsUnset()); + + if (IsActorDestroyed()) { + return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } + + mCursor->SendResponseInternal(mResponse, mFiles); + +#ifdef DEBUG + mResponseSent = true; +#endif + return NS_OK; +} + +Utils::Utils() +#ifdef DEBUG + : mActorDestroyed(false) +#endif +{ + AssertIsOnBackgroundThread(); +} + +Utils::~Utils() +{ + MOZ_ASSERT(mActorDestroyed); +} + +void +Utils::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + +#ifdef DEBUG + mActorDestroyed = true; +#endif +} + +bool +Utils::RecvDeleteMe() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + return PBackgroundIndexedDBUtilsParent::Send__delete__(this); +} + +bool +Utils::RecvGetFileReferences(const PersistenceType& aPersistenceType, + const nsCString& aOrigin, + const nsString& aDatabaseName, + const int64_t& aFileId, + int32_t* aRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt, + bool* aResult) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aRefCnt); + MOZ_ASSERT(aDBRefCnt); + MOZ_ASSERT(aSliceRefCnt); + MOZ_ASSERT(aResult); + MOZ_ASSERT(!mActorDestroyed); + + if (NS_WARN_IF(!IndexedDatabaseManager::Get() || + !QuotaManager::Get())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(!IndexedDatabaseManager::InTestingMode())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(aPersistenceType != quota::PERSISTENCE_TYPE_PERSISTENT && + aPersistenceType != quota::PERSISTENCE_TYPE_TEMPORARY && + aPersistenceType != quota::PERSISTENCE_TYPE_DEFAULT)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(aOrigin.IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(aDatabaseName.IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF(aFileId == 0)) { + ASSERT_UNLESS_FUZZING(); + return false; + } + + RefPtr<GetFileReferencesHelper> helper = + new GetFileReferencesHelper(aPersistenceType, aOrigin, aDatabaseName, + aFileId); + + nsresult rv = + helper->DispatchAndReturnFileReferences(aRefCnt, aDBRefCnt, + aSliceRefCnt, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +nsresult +GetFileReferencesHelper::DispatchAndReturnFileReferences(int32_t* aMemRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt, + bool* aResult) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aMemRefCnt); + MOZ_ASSERT(aDBRefCnt); + MOZ_ASSERT(aSliceRefCnt); + MOZ_ASSERT(aResult); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsresult rv = + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mozilla::MutexAutoLock autolock(mMutex); + while (mWaiting) { + mCondVar.Wait(); + } + + *aMemRefCnt = mMemRefCnt; + *aDBRefCnt = mDBRefCnt; + *aSliceRefCnt = mSliceRefCnt; + *aResult = mResult; + + return NS_OK; +} + +NS_IMETHODIMP +GetFileReferencesHelper::Run() +{ + AssertIsOnIOThread(); + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); + MOZ_ASSERT(mgr); + + RefPtr<FileManager> fileManager = + mgr->GetFileManager(mPersistenceType, mOrigin, mDatabaseName); + + if (fileManager) { + RefPtr<FileInfo> fileInfo = fileManager->GetFileInfo(mFileId); + + if (fileInfo) { + fileInfo->GetReferences(&mMemRefCnt, &mDBRefCnt, &mSliceRefCnt); + + if (mMemRefCnt != -1) { + // We added an extra temp ref, so account for that accordingly. + mMemRefCnt--; + } + + mResult = true; + } + } + + mozilla::MutexAutoLock lock(mMutex); + MOZ_ASSERT(mWaiting); + + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +NS_IMETHODIMP +FlushPendingFileDeletionsRunnable::Run() +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<IndexedDatabaseManager> mgr = IndexedDatabaseManager::Get(); + if (NS_WARN_IF(!mgr)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = mgr->FlushPendingFileDeletions(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +PermissionRequestHelper::OnPromptComplete(PermissionValue aPermissionValue) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mActorDestroyed) { + Unused << + PIndexedDBPermissionRequestParent::Send__delete__(this, aPermissionValue); + } +} + +void +PermissionRequestHelper::ActorDestroy(ActorDestroyReason aWhy) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mActorDestroyed); + + mActorDestroyed = true; +} + +#ifdef DEBUG + +NS_IMPL_ISUPPORTS(DEBUGThreadSlower, nsIThreadObserver) + +NS_IMETHODIMP +DEBUGThreadSlower::OnDispatchedEvent(nsIThreadInternal* /* aThread */) +{ + MOZ_CRASH("Should never be called!"); +} + +NS_IMETHODIMP +DEBUGThreadSlower::OnProcessNextEvent(nsIThreadInternal* /* aThread */, + bool /* aMayWait */) +{ + return NS_OK; +} + +NS_IMETHODIMP +DEBUGThreadSlower::AfterProcessNextEvent(nsIThreadInternal* /* aThread */, + bool /* aEventWasProcessed */) +{ + MOZ_ASSERT(kDEBUGThreadSleepMS); + + MOZ_ALWAYS_TRUE(PR_Sleep(PR_MillisecondsToInterval(kDEBUGThreadSleepMS)) == + PR_SUCCESS); + return NS_OK; +} + +#endif // DEBUG + +nsresult +FileHelper::Init() +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mFileManager); + + nsCOMPtr<nsIFile> fileDirectory = mFileManager->GetCheckedDirectory(); + if (NS_WARN_IF(!fileDirectory)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> journalDirectory = mFileManager->EnsureJournalDirectory(); + if (NS_WARN_IF(!journalDirectory)) { + return NS_ERROR_FAILURE; + } + + DebugOnly<bool> exists; + MOZ_ASSERT(NS_SUCCEEDED(journalDirectory->Exists(&exists))); + MOZ_ASSERT(exists); + + DebugOnly<bool> isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(journalDirectory->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory); + + mFileDirectory = Move(fileDirectory); + mJournalDirectory= Move(journalDirectory); + + return NS_OK; +} + +already_AddRefed<nsIFile> +FileHelper::GetFile(FileInfo* aFileInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFileInfo); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(mFileDirectory); + + const int64_t fileId = aFileInfo->Id(); + MOZ_ASSERT(fileId > 0); + + nsCOMPtr<nsIFile> file = + mFileManager->GetFileForId(mFileDirectory, fileId); + return file.forget(); +} + +already_AddRefed<nsIFile> +FileHelper::GetCheckedFile(FileInfo* aFileInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFileInfo); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(mFileDirectory); + + const int64_t fileId = aFileInfo->Id(); + MOZ_ASSERT(fileId > 0); + + nsCOMPtr<nsIFile> file = + mFileManager->GetCheckedFileForId(mFileDirectory, fileId); + return file.forget(); +} + +already_AddRefed<nsIFile> +FileHelper::GetJournalFile(FileInfo* aFileInfo) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFileInfo); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(mJournalDirectory); + + const int64_t fileId = aFileInfo->Id(); + MOZ_ASSERT(fileId > 0); + + nsCOMPtr<nsIFile> file = + mFileManager->GetFileForId(mJournalDirectory, fileId); + return file.forget(); +} + +nsresult +FileHelper::CreateFileFromStream(nsIFile* aFile, + nsIFile* aJournalFile, + nsIInputStream* aInputStream, + bool aCompress) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFile); + MOZ_ASSERT(aJournalFile); + MOZ_ASSERT(aInputStream); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(mFileDirectory); + MOZ_ASSERT(mJournalDirectory); + + bool exists; + nsresult rv = aFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // DOM blobs that are being stored in IDB are cached by calling + // IDBDatabase::GetOrCreateFileActorForBlob. So if the same DOM blob is stored + // again under a different key or in a different object store, we just add + // a new reference instead of creating a new copy (all such stored blobs share + // the same id). + // However, it can happen that CreateFileFromStream failed due to quota + // exceeded error and for some reason the orphaned file couldn't be deleted + // immediately. Now, if the operation is being repeated, the DOM blob is + // already cached, so it has the same file id which clashes with the orphaned + // file. We could do some tricks to restore previous copy loop, but it's safer + // to just delete the orphaned file and start from scratch. + // This corner case is partially simulated in test_file_copy_failure.js + if (exists) { + bool isFile; + rv = aFile->IsFile(&isFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isFile)) { + return NS_ERROR_FAILURE; + } + + rv = aJournalFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!exists)) { + return NS_ERROR_FAILURE; + } + + rv = aJournalFile->IsFile(&isFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!isFile)) { + return NS_ERROR_FAILURE; + } + + IDB_WARNING("Deleting orphaned file!"); + + rv = RemoveFile(aFile, aJournalFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Create a journal file first. + rv = aJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now try to copy the stream. + RefPtr<FileOutputStream> fileOutputStream = + FileOutputStream::Create(mFileManager->Type(), + mFileManager->Group(), + mFileManager->Origin(), + aFile); + if (NS_WARN_IF(!fileOutputStream)) { + return NS_ERROR_FAILURE; + } + + if (aCompress) { + RefPtr<SnappyCompressOutputStream> snappyOutputStream = + new SnappyCompressOutputStream(fileOutputStream); + + UniquePtr<char[]> buffer(new char[snappyOutputStream->BlockSize()]); + + rv = SyncCopy(aInputStream, + snappyOutputStream, + buffer.get(), + snappyOutputStream->BlockSize()); + } else { + char buffer[kFileCopyBufferSize]; + + rv = SyncCopy(aInputStream, + fileOutputStream, + buffer, + kFileCopyBufferSize); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +FileHelper::ReplaceFile(nsIFile* aFile, + nsIFile* aNewFile, + nsIFile* aNewJournalFile) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aFile); + MOZ_ASSERT(aNewFile); + MOZ_ASSERT(aNewJournalFile); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(mFileDirectory); + MOZ_ASSERT(mJournalDirectory); + + nsresult rv; + + int64_t fileSize; + + if (mFileManager->EnforcingQuota()) { + rv = aFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsAutoString fileName; + rv = aFile->GetLeafName(fileName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aNewFile->RenameTo(nullptr, fileName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mFileManager->EnforcingQuota()) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->DecreaseUsageForOrigin(mFileManager->Type(), + mFileManager->Group(), + mFileManager->Origin(), + fileSize); + } + + rv = aNewJournalFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +FileHelper::RemoveFile(nsIFile* aFile, + nsIFile* aJournalFile) +{ + nsresult rv; + + int64_t fileSize; + + if (mFileManager->EnforcingQuota()) { + rv = aFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + rv = aFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mFileManager->EnforcingQuota()) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->DecreaseUsageForOrigin(mFileManager->Type(), + mFileManager->Group(), + mFileManager->Origin(), + fileSize); + } + + rv = aJournalFile->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +already_AddRefed<FileInfo> +FileHelper::GetNewFileInfo() +{ + MOZ_ASSERT(mFileManager); + + return mFileManager->GetNewFileInfo(); +} + +nsresult +FileHelper::SyncCopy(nsIInputStream* aInputStream, + nsIOutputStream* aOutputStream, + char* aBuffer, + uint32_t aBufferSize) +{ + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aInputStream); + MOZ_ASSERT(aOutputStream); + + PROFILER_LABEL("IndexedDB", + "FileHelper::SyncCopy", + js::ProfileEntry::Category::STORAGE); + + nsresult rv; + + do { + uint32_t numRead; + rv = aInputStream->Read(aBuffer, aBufferSize, &numRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + if (!numRead) { + break; + } + + uint32_t numWrite; + rv = aOutputStream->Write(aBuffer, numRead, &numWrite); + if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { + rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + if (NS_WARN_IF(numWrite != numRead)) { + rv = NS_ERROR_FAILURE; + break; + } + } while (true); + + if (NS_SUCCEEDED(rv)) { + rv = aOutputStream->Flush(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsresult rv2 = aOutputStream->Close(); + if (NS_WARN_IF(NS_FAILED(rv2))) { + return NS_SUCCEEDED(rv) ? rv2 : rv; + } + + return rv; +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#undef IDB_MOBILE +#undef IDB_DEBUG_LOG +#undef ASSERT_UNLESS_FUZZING +#undef DISABLE_ASSERTS_FOR_FUZZING diff --git a/dom/indexedDB/ActorsParent.h b/dom/indexedDB/ActorsParent.h new file mode 100644 index 000000000..955d41b63 --- /dev/null +++ b/dom/indexedDB/ActorsParent.h @@ -0,0 +1,73 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_actorsparent_h__ +#define mozilla_dom_indexeddb_actorsparent_h__ + +template <class> struct already_AddRefed; +class nsIPrincipal; + +namespace mozilla { +namespace dom { + +class Element; +class FileHandleThreadPool; + +namespace quota { + +class Client; + +} // namespace quota + +namespace indexedDB { + +class LoggingInfo; +class PBackgroundIDBFactoryParent; +class PBackgroundIndexedDBUtilsParent; +class PIndexedDBPermissionRequestParent; + +PBackgroundIDBFactoryParent* +AllocPBackgroundIDBFactoryParent(const LoggingInfo& aLoggingInfo); + +bool +RecvPBackgroundIDBFactoryConstructor(PBackgroundIDBFactoryParent* aActor, + const LoggingInfo& aLoggingInfo); + +bool +DeallocPBackgroundIDBFactoryParent(PBackgroundIDBFactoryParent* aActor); + +PBackgroundIndexedDBUtilsParent* +AllocPBackgroundIndexedDBUtilsParent(); + +bool +DeallocPBackgroundIndexedDBUtilsParent(PBackgroundIndexedDBUtilsParent* aActor); + +bool +RecvFlushPendingFileDeletions(); + +PIndexedDBPermissionRequestParent* +AllocPIndexedDBPermissionRequestParent(Element* aOwnerElement, + nsIPrincipal* aPrincipal); + +bool +RecvPIndexedDBPermissionRequestConstructor( + PIndexedDBPermissionRequestParent* aActor); + +bool +DeallocPIndexedDBPermissionRequestParent( + PIndexedDBPermissionRequestParent* aActor); + +already_AddRefed<mozilla::dom::quota::Client> +CreateQuotaClient(); + +FileHandleThreadPool* +GetFileHandleThreadPool(); + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_actorsparent_h__ diff --git a/dom/indexedDB/FileInfo.cpp b/dom/indexedDB/FileInfo.cpp new file mode 100644 index 000000000..471007273 --- /dev/null +++ b/dom/indexedDB/FileInfo.cpp @@ -0,0 +1,260 @@ +/* -*- 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 "FileInfo.h" + +#include "FileManager.h" +#include "IndexedDatabaseManager.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "nsError.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +using namespace mozilla::dom::quota; + +namespace { + +template <typename IdType> +class FileInfoImpl final + : public FileInfo +{ + IdType mFileId; + +public: + FileInfoImpl(FileManager* aFileManager, IdType aFileId) + : FileInfo(aFileManager) + , mFileId(aFileId) + { + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aFileId > 0); + } + +private: + ~FileInfoImpl() + { } + + virtual int64_t + Id() const override + { + return int64_t(mFileId); + } +}; + +class CleanupFileRunnable final + : public Runnable +{ + RefPtr<FileManager> mFileManager; + int64_t mFileId; + +public: + static void + DoCleanup(FileManager* aFileManager, int64_t aFileId); + + CleanupFileRunnable(FileManager* aFileManager, int64_t aFileId) + : mFileManager(aFileManager) + , mFileId(aFileId) + { + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aFileId > 0); + } + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~CleanupFileRunnable() + { } + + NS_DECL_NSIRUNNABLE +}; + +} // namespace + +FileInfo::FileInfo(FileManager* aFileManager) + : mFileManager(aFileManager) +{ + MOZ_ASSERT(aFileManager); +} + +FileInfo::~FileInfo() +{ +} + +// static +FileInfo* +FileInfo::Create(FileManager* aFileManager, int64_t aId) +{ + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aId > 0); + + if (aId <= INT16_MAX) { + return new FileInfoImpl<int16_t>(aFileManager, aId); + } + + if (aId <= INT32_MAX) { + return new FileInfoImpl<int32_t>(aFileManager, aId); + } + + return new FileInfoImpl<int64_t>(aFileManager, aId); +} + +void +FileInfo::GetReferences(int32_t* aRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt) +{ + MOZ_ASSERT(!IndexedDatabaseManager::IsClosed()); + + MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); + + if (aRefCnt) { + *aRefCnt = mRefCnt; + } + + if (aDBRefCnt) { + *aDBRefCnt = mDBRefCnt; + } + + if (aSliceRefCnt) { + *aSliceRefCnt = mSliceRefCnt; + } +} + +void +FileInfo::UpdateReferences(ThreadSafeAutoRefCnt& aRefCount, + int32_t aDelta, + CustomCleanupCallback* aCustomCleanupCallback) +{ + // XXX This can go away once DOM objects no longer hold FileInfo objects... + // Looking at you, BlobImplBase... + // BlobImplBase is being addressed in bug 1068975. + if (IndexedDatabaseManager::IsClosed()) { + MOZ_ASSERT(&aRefCount == &mRefCnt); + MOZ_ASSERT(aDelta == 1 || aDelta == -1); + + if (aDelta > 0) { + ++aRefCount; + } else { + nsrefcnt count = --aRefCount; + if (!count) { + mRefCnt = 1; + delete this; + } + } + return; + } + + MOZ_ASSERT(!IndexedDatabaseManager::IsClosed()); + + bool needsCleanup; + { + MutexAutoLock lock(IndexedDatabaseManager::FileMutex()); + + aRefCount = aRefCount + aDelta; + + if (mRefCnt + mDBRefCnt + mSliceRefCnt > 0) { + return; + } + + mFileManager->mFileInfos.Remove(Id()); + + needsCleanup = !mFileManager->Invalidated(); + } + + if (needsCleanup) { + if (aCustomCleanupCallback) { + nsresult rv = aCustomCleanupCallback->Cleanup(mFileManager, Id()); + if (NS_FAILED(rv)) { + NS_WARNING("Custom cleanup failed!"); + } + } else { + Cleanup(); + } + } + + delete this; +} + +bool +FileInfo::LockedClearDBRefs() +{ + MOZ_ASSERT(!IndexedDatabaseManager::IsClosed()); + + IndexedDatabaseManager::FileMutex().AssertCurrentThreadOwns(); + + mDBRefCnt = 0; + + if (mRefCnt || mSliceRefCnt) { + return true; + } + + // In this case, we are not responsible for removing the file info from the + // hashtable. It's up to FileManager which is the only caller of this method. + + MOZ_ASSERT(mFileManager->Invalidated()); + + delete this; + + return false; +} + +void +FileInfo::Cleanup() +{ + int64_t id = Id(); + + // IndexedDatabaseManager is main-thread only. + if (!NS_IsMainThread()) { + RefPtr<CleanupFileRunnable> cleaner = + new CleanupFileRunnable(mFileManager, id); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(cleaner)); + return; + } + + CleanupFileRunnable::DoCleanup(mFileManager, id); +} + +// static +void +CleanupFileRunnable::DoCleanup(FileManager* aFileManager, int64_t aFileId) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aFileId > 0); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return; + } + + RefPtr<IndexedDatabaseManager> mgr = IndexedDatabaseManager::Get(); + MOZ_ASSERT(mgr); + + if (NS_FAILED(mgr->AsyncDeleteFile(aFileManager, aFileId))) { + NS_WARNING("Failed to delete file asynchronously!"); + } +} + +NS_IMPL_ISUPPORTS_INHERITED0(CleanupFileRunnable, Runnable) + +NS_IMETHODIMP +CleanupFileRunnable::Run() +{ + MOZ_ASSERT(NS_IsMainThread()); + + DoCleanup(mFileManager, mFileId); + + return NS_OK; +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/FileInfo.h b/dom/indexedDB/FileInfo.h new file mode 100644 index 000000000..e228d4ad7 --- /dev/null +++ b/dom/indexedDB/FileInfo.h @@ -0,0 +1,103 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_fileinfo_h__ +#define mozilla_dom_indexeddb_fileinfo_h__ + +#include "nsISupportsImpl.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +class FileManager; + +class FileInfo +{ + friend class FileManager; + + ThreadSafeAutoRefCnt mRefCnt; + ThreadSafeAutoRefCnt mDBRefCnt; + ThreadSafeAutoRefCnt mSliceRefCnt; + + RefPtr<FileManager> mFileManager; + +public: + class CustomCleanupCallback; + + static + FileInfo* Create(FileManager* aFileManager, int64_t aId); + + explicit FileInfo(FileManager* aFileManager); + + void + AddRef() + { + UpdateReferences(mRefCnt, 1); + } + + void + Release(CustomCleanupCallback* aCustomCleanupCallback = nullptr) + { + UpdateReferences(mRefCnt, -1, aCustomCleanupCallback); + } + + void + UpdateDBRefs(int32_t aDelta) + { + UpdateReferences(mDBRefCnt, aDelta); + } + + void + UpdateSliceRefs(int32_t aDelta) + { + UpdateReferences(mSliceRefCnt, aDelta); + } + + void + GetReferences(int32_t* aRefCnt, int32_t* aDBRefCnt, int32_t* aSliceRefCnt); + + FileManager* + Manager() const + { + return mFileManager; + } + + virtual int64_t + Id() const = 0; + +protected: + virtual ~FileInfo(); + +private: + void + UpdateReferences(ThreadSafeAutoRefCnt& aRefCount, + int32_t aDelta, + CustomCleanupCallback* aCustomCleanupCallback = nullptr); + + bool + LockedClearDBRefs(); + + void + Cleanup(); +}; + +class NS_NO_VTABLE FileInfo::CustomCleanupCallback +{ +public: + virtual nsresult + Cleanup(FileManager* aFileManager, int64_t aId) = 0; + +protected: + CustomCleanupCallback() + { } +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_fileinfo_h__ diff --git a/dom/indexedDB/FileManager.h b/dom/indexedDB/FileManager.h new file mode 100644 index 000000000..da917f431 --- /dev/null +++ b/dom/indexedDB/FileManager.h @@ -0,0 +1,150 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_filemanager_h__ +#define mozilla_dom_indexeddb_filemanager_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "nsDataHashtable.h" +#include "nsHashKeys.h" +#include "nsISupportsImpl.h" + +class nsIFile; +class mozIStorageConnection; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +class FileInfo; + +// Implemented in ActorsParent.cpp. +class FileManager final +{ + friend class FileInfo; + + typedef mozilla::dom::quota::PersistenceType PersistenceType; + + PersistenceType mPersistenceType; + nsCString mGroup; + nsCString mOrigin; + nsString mDatabaseName; + + nsString mDirectoryPath; + nsString mJournalDirectoryPath; + + int64_t mLastFileId; + + // Protected by IndexedDatabaseManager::FileMutex() + nsDataHashtable<nsUint64HashKey, FileInfo*> mFileInfos; + + const bool mIsApp; + const bool mEnforcingQuota; + bool mInvalidated; + +public: + static already_AddRefed<nsIFile> + GetFileForId(nsIFile* aDirectory, int64_t aId); + + static already_AddRefed<nsIFile> + GetCheckedFileForId(nsIFile* aDirectory, int64_t aId); + + static nsresult + InitDirectory(nsIFile* aDirectory, + nsIFile* aDatabaseFile, + PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + uint32_t aTelemetryId); + + static nsresult + GetUsage(nsIFile* aDirectory, uint64_t* aUsage); + + FileManager(PersistenceType aPersistenceType, + const nsACString& aGroup, + const nsACString& aOrigin, + bool aIsApp, + const nsAString& aDatabaseName, + bool aEnforcingQuota); + + PersistenceType + Type() const + { + return mPersistenceType; + } + + const nsACString& + Group() const + { + return mGroup; + } + + const nsACString& + Origin() const + { + return mOrigin; + } + + bool + IsApp() const + { + return mIsApp; + } + + const nsAString& + DatabaseName() const + { + return mDatabaseName; + } + + bool + EnforcingQuota() const + { + return mEnforcingQuota; + } + + bool + Invalidated() const + { + return mInvalidated; + } + + nsresult + Init(nsIFile* aDirectory, mozIStorageConnection* aConnection); + + nsresult + Invalidate(); + + already_AddRefed<nsIFile> + GetDirectory(); + + already_AddRefed<nsIFile> + GetCheckedDirectory(); + + already_AddRefed<nsIFile> + GetJournalDirectory(); + + already_AddRefed<nsIFile> + EnsureJournalDirectory(); + + already_AddRefed<FileInfo> + GetFileInfo(int64_t aId); + + already_AddRefed<FileInfo> + GetNewFileInfo(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileManager) + +private: + ~FileManager(); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_filemanager_h__ diff --git a/dom/indexedDB/FileSnapshot.cpp b/dom/indexedDB/FileSnapshot.cpp new file mode 100644 index 000000000..986de074e --- /dev/null +++ b/dom/indexedDB/FileSnapshot.cpp @@ -0,0 +1,311 @@ +/* -*- 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 "FileSnapshot.h" + +#include "IDBFileHandle.h" +#include "mozilla/Assertions.h" +#include "nsIIPCSerializableInputStream.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +using namespace mozilla::ipc; + +namespace { + +class StreamWrapper final + : public nsIInputStream + , public nsIIPCSerializableInputStream +{ + class CloseRunnable; + + nsCOMPtr<nsIEventTarget> mOwningThread; + nsCOMPtr<nsIInputStream> mInputStream; + RefPtr<IDBFileHandle> mFileHandle; + bool mFinished; + +public: + StreamWrapper(nsIInputStream* aInputStream, + IDBFileHandle* aFileHandle) + : mOwningThread(NS_GetCurrentThread()) + , mInputStream(aInputStream) + , mFileHandle(aFileHandle) + , mFinished(false) + { + AssertIsOnOwningThread(); + MOZ_ASSERT(aInputStream); + MOZ_ASSERT(aFileHandle); + aFileHandle->AssertIsOnOwningThread(); + + mFileHandle->OnNewRequest(); + } + +private: + virtual ~StreamWrapper(); + + bool + IsOnOwningThread() const + { + MOZ_ASSERT(mOwningThread); + + bool current; + return NS_SUCCEEDED(mOwningThread-> + IsOnCurrentThread(¤t)) && current; + } + + void + AssertIsOnOwningThread() const + { + MOZ_ASSERT(IsOnOwningThread()); + } + + void + Finish() + { + AssertIsOnOwningThread(); + + if (mFinished) { + return; + } + + mFinished = true; + + mFileHandle->OnRequestFinished(/* aActorDestroyedNormally */ true); + } + + void + Destroy() + { + if (IsOnOwningThread()) { + delete this; + return; + } + + nsCOMPtr<nsIRunnable> destroyRunnable = + NewNonOwningRunnableMethod(this, &StreamWrapper::Destroy); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(destroyRunnable, + NS_DISPATCH_NORMAL)); + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIIPCSERIALIZABLEINPUTSTREAM +}; + +class StreamWrapper::CloseRunnable final + : public Runnable +{ + friend class StreamWrapper; + + RefPtr<StreamWrapper> mStreamWrapper; + +public: + NS_DECL_ISUPPORTS_INHERITED + +private: + explicit + CloseRunnable(StreamWrapper* aStreamWrapper) + : mStreamWrapper(aStreamWrapper) + { } + + ~CloseRunnable() + { } + + NS_IMETHOD + Run() override; +}; + +} // anonymous namespace + +BlobImplSnapshot::BlobImplSnapshot(BlobImpl* aFileImpl, + IDBFileHandle* aFileHandle) + : mBlobImpl(aFileImpl) +{ + MOZ_ASSERT(aFileImpl); + MOZ_ASSERT(aFileHandle); + + mFileHandle = + do_GetWeakReference(NS_ISUPPORTS_CAST(EventTarget*, aFileHandle)); +} + +BlobImplSnapshot::BlobImplSnapshot(BlobImpl* aFileImpl, + nsIWeakReference* aFileHandle) + : mBlobImpl(aFileImpl) + , mFileHandle(aFileHandle) +{ + MOZ_ASSERT(aFileImpl); + MOZ_ASSERT(aFileHandle); +} + +BlobImplSnapshot::~BlobImplSnapshot() +{ +} + +NS_IMPL_ISUPPORTS_INHERITED(BlobImplSnapshot, BlobImpl, PIBlobImplSnapshot) + +already_AddRefed<BlobImpl> +BlobImplSnapshot::CreateSlice(uint64_t aStart, + uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) +{ + RefPtr<BlobImpl> blobImpl = + mBlobImpl->CreateSlice(aStart, aLength, aContentType, aRv); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + blobImpl = new BlobImplSnapshot(blobImpl, mFileHandle); + return blobImpl.forget(); +} + +void +BlobImplSnapshot::GetInternalStream(nsIInputStream** aStream, ErrorResult& aRv) +{ + nsCOMPtr<EventTarget> et = do_QueryReferent(mFileHandle); + RefPtr<IDBFileHandle> fileHandle = static_cast<IDBFileHandle*>(et.get()); + if (!fileHandle || !fileHandle->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_FILEHANDLE_INACTIVE_ERR); + return; + } + + nsCOMPtr<nsIInputStream> stream; + mBlobImpl->GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<StreamWrapper> wrapper = new StreamWrapper(stream, fileHandle); + + wrapper.forget(aStream); +} + +BlobImpl* +BlobImplSnapshot::GetBlobImpl() const +{ + nsCOMPtr<EventTarget> et = do_QueryReferent(mFileHandle); + RefPtr<IDBFileHandle> fileHandle = static_cast<IDBFileHandle*>(et.get()); + if (!fileHandle || !fileHandle->IsOpen()) { + return nullptr; + } + + return mBlobImpl; +} + +StreamWrapper::~StreamWrapper() +{ + AssertIsOnOwningThread(); + + Finish(); +} + +NS_IMPL_ADDREF(StreamWrapper) +NS_IMPL_RELEASE_WITH_DESTROY(StreamWrapper, Destroy()) +NS_IMPL_QUERY_INTERFACE(StreamWrapper, + nsIInputStream, + nsIIPCSerializableInputStream) + +NS_IMETHODIMP +StreamWrapper::Close() +{ + MOZ_ASSERT(!IsOnOwningThread()); + + RefPtr<CloseRunnable> closeRunnable = new CloseRunnable(this); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(closeRunnable, + NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +NS_IMETHODIMP +StreamWrapper::Available(uint64_t* _retval) +{ + // Can't assert here, this method is sometimes called on the owning thread + // (nsInputStreamChannel::OpenContentStream calls Available before setting + // the content length property). + + return mInputStream->Available(_retval); +} + +NS_IMETHODIMP +StreamWrapper::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) +{ + MOZ_ASSERT(!IsOnOwningThread()); + return mInputStream->Read(aBuf, aCount, _retval); +} + +NS_IMETHODIMP +StreamWrapper::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) +{ + MOZ_ASSERT(!IsOnOwningThread()); + return mInputStream->ReadSegments(aWriter, aClosure, aCount, _retval); +} + +NS_IMETHODIMP +StreamWrapper::IsNonBlocking(bool* _retval) +{ + return mInputStream->IsNonBlocking(_retval); +} + +void +StreamWrapper::Serialize(InputStreamParams& aParams, + FileDescriptorArray& aFileDescriptors) +{ + nsCOMPtr<nsIIPCSerializableInputStream> stream = + do_QueryInterface(mInputStream); + + if (stream) { + stream->Serialize(aParams, aFileDescriptors); + } +} + +bool +StreamWrapper::Deserialize(const InputStreamParams& aParams, + const FileDescriptorArray& aFileDescriptors) +{ + nsCOMPtr<nsIIPCSerializableInputStream> stream = + do_QueryInterface(mInputStream); + + if (stream) { + return stream->Deserialize(aParams, aFileDescriptors); + } + + return false; +} + +Maybe<uint64_t> +StreamWrapper::ExpectedSerializedLength() +{ + nsCOMPtr<nsIIPCSerializableInputStream> stream = + do_QueryInterface(mInputStream); + + if (stream) { + return stream->ExpectedSerializedLength(); + } + return Nothing(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(StreamWrapper::CloseRunnable, + Runnable) + +NS_IMETHODIMP +StreamWrapper:: +CloseRunnable::Run() +{ + mStreamWrapper->Finish(); + + return NS_OK; +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/FileSnapshot.h b/dom/indexedDB/FileSnapshot.h new file mode 100644 index 000000000..6b577fcc3 --- /dev/null +++ b/dom/indexedDB/FileSnapshot.h @@ -0,0 +1,210 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_filesnapshot_h__ +#define mozilla_dom_indexeddb_filesnapshot_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/File.h" +#include "nsISupports.h" +#include "nsWeakPtr.h" + +#define FILEIMPLSNAPSHOT_IID \ + {0x0dfc11b1, 0x75d3, 0x473b, {0x8c, 0x67, 0xb7, 0x23, 0xf4, 0x67, 0xd6, 0x73}} + +class PIBlobImplSnapshot : public nsISupports +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(FILEIMPLSNAPSHOT_IID) + + virtual mozilla::dom::BlobImpl* + GetBlobImpl() const = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(PIBlobImplSnapshot, FILEIMPLSNAPSHOT_IID) + +namespace mozilla { +namespace dom { + +class IDBFileHandle; + +namespace indexedDB { + +class BlobImplSnapshot final + : public BlobImpl + , public PIBlobImplSnapshot +{ + RefPtr<BlobImpl> mBlobImpl; + nsWeakPtr mFileHandle; + +public: + BlobImplSnapshot(BlobImpl* aImpl, + IDBFileHandle* aFileHandle); + + NS_DECL_ISUPPORTS_INHERITED + +private: + BlobImplSnapshot(BlobImpl* aImpl, + nsIWeakReference* aFileHandle); + + ~BlobImplSnapshot(); + + // BlobImpl + virtual void + GetName(nsAString& aName) const override + { + mBlobImpl->GetName(aName); + } + + virtual void + GetDOMPath(nsAString& aPath) const override + { + mBlobImpl->GetDOMPath(aPath); + } + + virtual void + SetDOMPath(const nsAString& aPath) override + { + mBlobImpl->SetDOMPath(aPath); + } + + virtual int64_t + GetLastModified(ErrorResult& aRv) override + { + return mBlobImpl->GetLastModified(aRv); + } + + virtual void + SetLastModified(int64_t aLastModified) override + { + mBlobImpl->SetLastModified(aLastModified); + } + + virtual void + GetMozFullPath(nsAString& aName, ErrorResult& aRv) const override + { + mBlobImpl->GetMozFullPath(aName, aRv); + } + + virtual void + GetMozFullPathInternal(nsAString& aFileName, ErrorResult& aRv) const override + { + mBlobImpl->GetMozFullPathInternal(aFileName, aRv); + } + + virtual uint64_t + GetSize(ErrorResult& aRv) override + { + return mBlobImpl->GetSize(aRv); + } + + virtual void + GetType(nsAString& aType) override + { + mBlobImpl->GetType(aType); + } + + virtual uint64_t + GetSerialNumber() const override + { + return mBlobImpl->GetSerialNumber(); + } + + virtual already_AddRefed<BlobImpl> + CreateSlice(uint64_t aStart, + uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) override; + + virtual const nsTArray<RefPtr<BlobImpl>>* + GetSubBlobImpls() const override + { + return mBlobImpl->GetSubBlobImpls(); + } + + virtual void + GetInternalStream(nsIInputStream** aStream, + ErrorResult& aRv) override; + + virtual int64_t + GetFileId() override + { + return mBlobImpl->GetFileId(); + } + + virtual nsresult + GetSendInfo(nsIInputStream** aBody, + uint64_t* aContentLength, + nsACString& aContentType, + nsACString& aCharset) override + { + return mBlobImpl->GetSendInfo(aBody, + aContentLength, + aContentType, + aCharset); + } + + virtual nsresult + GetMutable(bool* aMutable) const override + { + return mBlobImpl->GetMutable(aMutable); + } + + virtual nsresult + SetMutable(bool aMutable) override + { + return mBlobImpl->SetMutable(aMutable); + } + + virtual void + SetLazyData(const nsAString& aName, + const nsAString& aContentType, + uint64_t aLength, + int64_t aLastModifiedDate) override + { + MOZ_CRASH("This should never be called!"); + } + + virtual bool + IsMemoryFile() const override + { + return mBlobImpl->IsMemoryFile(); + } + + virtual bool + IsSizeUnknown() const override + { + return mBlobImpl->IsSizeUnknown(); + } + + virtual bool + IsDateUnknown() const override + { + return mBlobImpl->IsDateUnknown(); + } + + virtual bool + IsFile() const override + { + return mBlobImpl->IsFile(); + } + + virtual bool + MayBeClonedToOtherThreads() const override + { + return false; + } + + // PIBlobImplSnapshot + virtual BlobImpl* + GetBlobImpl() const override; +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_filesnapshot_h__ diff --git a/dom/indexedDB/IDBCursor.cpp b/dom/indexedDB/IDBCursor.cpp new file mode 100644 index 000000000..e5d8913f9 --- /dev/null +++ b/dom/indexedDB/IDBCursor.cpp @@ -0,0 +1,1006 @@ +/* -*- 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 "IDBCursor.h" + +#include "IDBDatabase.h" +#include "IDBIndex.h" +#include "IDBObjectStore.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabaseInlines.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "nsString.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { + +using namespace indexedDB; + +IDBCursor::IDBCursor(Type aType, + BackgroundCursorChild* aBackgroundActor, + const Key& aKey) + : mBackgroundActor(aBackgroundActor) + , mRequest(aBackgroundActor->GetRequest()) + , mSourceObjectStore(aBackgroundActor->GetObjectStore()) + , mSourceIndex(aBackgroundActor->GetIndex()) + , mTransaction(mRequest->GetTransaction()) + , mScriptOwner(mTransaction->Database()->GetScriptOwner()) + , mCachedKey(JS::UndefinedValue()) + , mCachedPrimaryKey(JS::UndefinedValue()) + , mCachedValue(JS::UndefinedValue()) + , mKey(aKey) + , mType(aType) + , mDirection(aBackgroundActor->GetDirection()) + , mHaveCachedKey(false) + , mHaveCachedPrimaryKey(false) + , mHaveCachedValue(false) + , mRooted(false) + , mContinueCalled(false) + , mHaveValue(true) +{ + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT_IF(aType == Type_ObjectStore || aType == Type_ObjectStoreKey, + mSourceObjectStore); + MOZ_ASSERT_IF(aType == Type_Index || aType == Type_IndexKey, mSourceIndex); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(!aKey.IsUnset()); + MOZ_ASSERT(mScriptOwner); + + if (mScriptOwner) { + mozilla::HoldJSObjects(this); + mRooted = true; + } +} + +#ifdef ENABLE_INTL_API +bool +IDBCursor::IsLocaleAware() const +{ + return mSourceIndex && !mSourceIndex->Locale().IsEmpty(); +} +#endif + +IDBCursor::~IDBCursor() +{ + AssertIsOnOwningThread(); + + DropJSObjects(); + + if (mBackgroundActor) { + mBackgroundActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } +} + +// static +already_AddRefed<IDBCursor> +IDBCursor::Create(BackgroundCursorChild* aBackgroundActor, + const Key& aKey, + StructuredCloneReadInfo&& aCloneInfo) +{ + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor->GetObjectStore()); + MOZ_ASSERT(!aBackgroundActor->GetIndex()); + MOZ_ASSERT(!aKey.IsUnset()); + + RefPtr<IDBCursor> cursor = + new IDBCursor(Type_ObjectStore, aBackgroundActor, aKey); + + cursor->mCloneInfo = Move(aCloneInfo); + + return cursor.forget(); +} + +// static +already_AddRefed<IDBCursor> +IDBCursor::Create(BackgroundCursorChild* aBackgroundActor, + const Key& aKey) +{ + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor->GetObjectStore()); + MOZ_ASSERT(!aBackgroundActor->GetIndex()); + MOZ_ASSERT(!aKey.IsUnset()); + + RefPtr<IDBCursor> cursor = + new IDBCursor(Type_ObjectStoreKey, aBackgroundActor, aKey); + + return cursor.forget(); +} + +// static +already_AddRefed<IDBCursor> +IDBCursor::Create(BackgroundCursorChild* aBackgroundActor, + const Key& aKey, + const Key& aSortKey, + const Key& aPrimaryKey, + StructuredCloneReadInfo&& aCloneInfo) +{ + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor->GetIndex()); + MOZ_ASSERT(!aBackgroundActor->GetObjectStore()); + MOZ_ASSERT(!aKey.IsUnset()); + MOZ_ASSERT(!aPrimaryKey.IsUnset()); + + RefPtr<IDBCursor> cursor = + new IDBCursor(Type_Index, aBackgroundActor, aKey); + + cursor->mSortKey = Move(aSortKey); + cursor->mPrimaryKey = Move(aPrimaryKey); + cursor->mCloneInfo = Move(aCloneInfo); + + return cursor.forget(); +} + +// static +already_AddRefed<IDBCursor> +IDBCursor::Create(BackgroundCursorChild* aBackgroundActor, + const Key& aKey, + const Key& aSortKey, + const Key& aPrimaryKey) +{ + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor->GetIndex()); + MOZ_ASSERT(!aBackgroundActor->GetObjectStore()); + MOZ_ASSERT(!aKey.IsUnset()); + MOZ_ASSERT(!aPrimaryKey.IsUnset()); + + RefPtr<IDBCursor> cursor = + new IDBCursor(Type_IndexKey, aBackgroundActor, aKey); + + cursor->mSortKey = Move(aSortKey); + cursor->mPrimaryKey = Move(aPrimaryKey); + + return cursor.forget(); +} + +// static +auto +IDBCursor::ConvertDirection(IDBCursorDirection aDirection) -> Direction +{ + switch (aDirection) { + case mozilla::dom::IDBCursorDirection::Next: + return NEXT; + + case mozilla::dom::IDBCursorDirection::Nextunique: + return NEXT_UNIQUE; + + case mozilla::dom::IDBCursorDirection::Prev: + return PREV; + + case mozilla::dom::IDBCursorDirection::Prevunique: + return PREV_UNIQUE; + + default: + MOZ_CRASH("Unknown direction!"); + } +} + +#ifdef DEBUG + +void +IDBCursor::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void +IDBCursor::DropJSObjects() +{ + AssertIsOnOwningThread(); + + Reset(); + + if (!mRooted) { + return; + } + + mScriptOwner = nullptr; + mRooted = false; + + mozilla::DropJSObjects(this); +} + +bool +IDBCursor::IsSourceDeleted() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mTransaction->IsOpen()); + + IDBObjectStore* sourceObjectStore; + if (mType == Type_Index || mType == Type_IndexKey) { + MOZ_ASSERT(mSourceIndex); + + if (mSourceIndex->IsDeleted()) { + return true; + } + + sourceObjectStore = mSourceIndex->ObjectStore(); + MOZ_ASSERT(sourceObjectStore); + } else { + MOZ_ASSERT(mSourceObjectStore); + sourceObjectStore = mSourceObjectStore; + } + + return sourceObjectStore->IsDeleted(); +} + +void +IDBCursor::Reset() +{ + AssertIsOnOwningThread(); + + mCachedKey.setUndefined(); + mCachedPrimaryKey.setUndefined(); + mCachedValue.setUndefined(); + IDBObjectStore::ClearCloneReadInfo(mCloneInfo); + + mHaveCachedKey = false; + mHaveCachedPrimaryKey = false; + mHaveCachedValue = false; + mHaveValue = false; + mContinueCalled = false; +} + +nsPIDOMWindowInner* +IDBCursor::GetParentObject() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + return mTransaction->GetParentObject(); +} + +IDBCursorDirection +IDBCursor::GetDirection() const +{ + AssertIsOnOwningThread(); + + switch (mDirection) { + case NEXT: + return IDBCursorDirection::Next; + + case NEXT_UNIQUE: + return IDBCursorDirection::Nextunique; + + case PREV: + return IDBCursorDirection::Prev; + + case PREV_UNIQUE: + return IDBCursorDirection::Prevunique; + + default: + MOZ_CRASH("Bad direction!"); + } +} + +void +IDBCursor::GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const +{ + AssertIsOnOwningThread(); + + switch (mType) { + case Type_ObjectStore: + case Type_ObjectStoreKey: + MOZ_ASSERT(mSourceObjectStore); + aSource.SetAsIDBObjectStore() = mSourceObjectStore; + return; + + case Type_Index: + case Type_IndexKey: + MOZ_ASSERT(mSourceIndex); + aSource.SetAsIDBIndex() = mSourceIndex; + return; + + default: + MOZ_ASSERT_UNREACHABLE("Bad type!"); + } +} + +void +IDBCursor::GetKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mKey.IsUnset() || !mHaveValue); + + if (!mHaveValue) { + aResult.setUndefined(); + return; + } + + if (!mHaveCachedKey) { + if (!mRooted) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + aRv = mKey.ToJSVal(aCx, mCachedKey); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mHaveCachedKey = true; + } + + aResult.set(mCachedKey); +} + +void +IDBCursor::GetPrimaryKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mHaveValue) { + aResult.setUndefined(); + return; + } + + if (!mHaveCachedPrimaryKey) { + if (!mRooted) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + const Key& key = + (mType == Type_ObjectStore || mType == Type_ObjectStoreKey) ? + mKey : + mPrimaryKey; + + MOZ_ASSERT(!key.IsUnset()); + + aRv = key.ToJSVal(aCx, mCachedPrimaryKey); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mHaveCachedPrimaryKey = true; + } + + aResult.set(mCachedPrimaryKey); +} + +void +IDBCursor::GetValue(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mType == Type_ObjectStore || mType == Type_Index); + + if (!mHaveValue) { + aResult.setUndefined(); + return; + } + + if (!mHaveCachedValue) { + if (!mRooted) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + JS::Rooted<JS::Value> val(aCx); + if (NS_WARN_IF(!IDBObjectStore::DeserializeValue(aCx, mCloneInfo, &val))) { + aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); + return; + } + + IDBObjectStore::ClearCloneReadInfo(mCloneInfo); + + mCachedValue = val; + mHaveCachedValue = true; + } + + aResult.set(mCachedValue); +} + +void +IDBCursor::Continue(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult &aRv) +{ + AssertIsOnOwningThread(); + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + if (IsSourceDeleted() || !mHaveValue || mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + Key key; + aRv = key.SetFromJSVal(aCx, aKey); + if (aRv.Failed()) { + return; + } + +#ifdef ENABLE_INTL_API + if (IsLocaleAware() && !key.IsUnset()) { + Key tmp; + aRv = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale()); + if (aRv.Failed()) { + return; + } + key = tmp; + } + + const Key& sortKey = IsLocaleAware() ? mSortKey : mKey; +#else + const Key& sortKey = mKey; +#endif + + if (!key.IsUnset()) { + switch (mDirection) { + case NEXT: + case NEXT_UNIQUE: + if (key <= sortKey) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + break; + + case PREV: + case PREV_UNIQUE: + if (key >= sortKey) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + break; + + default: + MOZ_CRASH("Unknown direction type!"); + } + } + + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + mRequest->SetLoggingSerialNumber(requestSerialNumber); + + if (mType == Type_ObjectStore || mType == Type_ObjectStoreKey) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).continue(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.continue()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(mSourceObjectStore), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(key)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).continue(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.continue()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()), + IDB_LOG_STRINGIFY(mSourceIndex), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(key)); + } + + mBackgroundActor->SendContinueInternal(ContinueParams(key)); + + mContinueCalled = true; +} + +void +IDBCursor::ContinuePrimaryKey(JSContext* aCx, + JS::Handle<JS::Value> aKey, + JS::Handle<JS::Value> aPrimaryKey, + ErrorResult &aRv) +{ + AssertIsOnOwningThread(); + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + if (IsSourceDeleted()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + if ((mType != Type_Index && mType != Type_IndexKey) || + (mDirection != NEXT && mDirection != PREV)) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return; + } + + if (!mHaveValue || mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + Key key; + aRv = key.SetFromJSVal(aCx, aKey); + if (aRv.Failed()) { + return; + } + +#ifdef ENABLE_INTL_API + if (IsLocaleAware() && !key.IsUnset()) { + Key tmp; + aRv = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale()); + if (aRv.Failed()) { + return; + } + key = tmp; + } + + const Key& sortKey = IsLocaleAware() ? mSortKey : mKey; +#else + const Key& sortKey = mKey; +#endif + + if (key.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + + Key primaryKey; + aRv = primaryKey.SetFromJSVal(aCx, aPrimaryKey); + if (aRv.Failed()) { + return; + } + + if (primaryKey.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + + switch (mDirection) { + case NEXT: + if (key < sortKey || + (key == sortKey && primaryKey <= mPrimaryKey)) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + break; + + case PREV: + if (key > sortKey || + (key == sortKey && primaryKey >= mPrimaryKey)) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + break; + + default: + MOZ_CRASH("Unknown direction type!"); + } + + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + mRequest->SetLoggingSerialNumber(requestSerialNumber); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).continuePrimaryKey(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.continuePrimaryKey()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()), + IDB_LOG_STRINGIFY(mSourceIndex), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(key), + IDB_LOG_STRINGIFY(primaryKey)); + + mBackgroundActor->SendContinueInternal(ContinuePrimaryKeyParams(key, primaryKey)); + + mContinueCalled = true; +} + +void +IDBCursor::Advance(uint32_t aCount, ErrorResult &aRv) +{ + AssertIsOnOwningThread(); + + if (!aCount) { + aRv.ThrowTypeError<MSG_INVALID_ADVANCE_COUNT>(); + return; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + + if (IsSourceDeleted() || !mHaveValue || mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + mRequest->SetLoggingSerialNumber(requestSerialNumber); + + if (mType == Type_ObjectStore || mType == Type_ObjectStoreKey) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).advance(%ld)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.advance()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(mSourceObjectStore), + IDB_LOG_STRINGIFY(mDirection), + aCount); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).advance(%ld)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.advance()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()), + IDB_LOG_STRINGIFY(mSourceIndex), + IDB_LOG_STRINGIFY(mDirection), + aCount); + } + + mBackgroundActor->SendContinueInternal(AdvanceParams(aCount)); + + mContinueCalled = true; +} + +already_AddRefed<IDBRequest> +IDBCursor::Update(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + if (!mTransaction->IsWriteAllowed()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); + return nullptr; + } + + if (mTransaction->GetMode() == IDBTransaction::CLEANUP || + IsSourceDeleted() || + !mHaveValue || + mType == Type_ObjectStoreKey || + mType == Type_IndexKey || + mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + MOZ_ASSERT(mType == Type_ObjectStore || mType == Type_Index); + MOZ_ASSERT(!mKey.IsUnset()); + MOZ_ASSERT_IF(mType == Type_Index, !mPrimaryKey.IsUnset()); + + IDBObjectStore* objectStore; + if (mType == Type_ObjectStore) { + objectStore = mSourceObjectStore; + } else { + objectStore = mSourceIndex->ObjectStore(); + } + + MOZ_ASSERT(objectStore); + + const Key& primaryKey = (mType == Type_ObjectStore) ? mKey : mPrimaryKey; + + RefPtr<IDBRequest> request; + + if (objectStore->HasValidKeyPath()) { + // Make sure the object given has the correct keyPath value set on it. + const KeyPath& keyPath = objectStore->GetKeyPath(); + Key key; + + aRv = keyPath.ExtractKey(aCx, aValue, key); + if (aRv.Failed()) { + return nullptr; + } + + if (key != primaryKey) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + request = objectStore->AddOrPut(aCx, + aValue, + /* aKey */ JS::UndefinedHandleValue, + /* aOverwrite */ true, + /* aFromCursor */ true, + aRv); + if (aRv.Failed()) { + return nullptr; + } + } + else { + JS::Rooted<JS::Value> keyVal(aCx); + aRv = primaryKey.ToJSVal(aCx, &keyVal); + if (aRv.Failed()) { + return nullptr; + } + + request = objectStore->AddOrPut(aCx, + aValue, + keyVal, + /* aOverwrite */ true, + /* aFromCursor */ true, + aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + request->SetSource(this); + + if (mType == Type_ObjectStore) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).update(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.update()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(objectStore), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(objectStore, primaryKey)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).update(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.update()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(objectStore), + IDB_LOG_STRINGIFY(mSourceIndex), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(objectStore, primaryKey)); + } + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBCursor::Delete(JSContext* aCx, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + if (!mTransaction->IsWriteAllowed()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); + return nullptr; + } + + if (IsSourceDeleted() || + !mHaveValue || + mType == Type_ObjectStoreKey || + mType == Type_IndexKey || + mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + MOZ_ASSERT(mType == Type_ObjectStore || mType == Type_Index); + MOZ_ASSERT(!mKey.IsUnset()); + + IDBObjectStore* objectStore; + if (mType == Type_ObjectStore) { + objectStore = mSourceObjectStore; + } else { + objectStore = mSourceIndex->ObjectStore(); + } + + MOZ_ASSERT(objectStore); + + const Key& primaryKey = (mType == Type_ObjectStore) ? mKey : mPrimaryKey; + + JS::Rooted<JS::Value> key(aCx); + aRv = primaryKey.ToJSVal(aCx, &key); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<IDBRequest> request = + objectStore->DeleteInternal(aCx, key, /* aFromCursor */ true, aRv); + if (aRv.Failed()) { + return nullptr; + } + + request->SetSource(this); + + if (mType == Type_ObjectStore) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).delete(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.delete()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(objectStore), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(objectStore, primaryKey)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).delete(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.delete()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(objectStore), + IDB_LOG_STRINGIFY(mSourceIndex), + IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(objectStore, primaryKey)); + } + + return request.forget(); +} + +void +IDBCursor::Reset(Key&& aKey, StructuredCloneReadInfo&& aValue) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mType == Type_ObjectStore); + + Reset(); + + mKey = Move(aKey); + mCloneInfo = Move(aValue); + + mHaveValue = !mKey.IsUnset(); +} + +void +IDBCursor::Reset(Key&& aKey) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mType == Type_ObjectStoreKey); + + Reset(); + + mKey = Move(aKey); + + mHaveValue = !mKey.IsUnset(); +} + +void +IDBCursor::Reset(Key&& aKey, + Key&& aSortKey, + Key&& aPrimaryKey, + StructuredCloneReadInfo&& aValue) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mType == Type_Index); + + Reset(); + + mKey = Move(aKey); + mSortKey = Move(aSortKey); + mPrimaryKey = Move(aPrimaryKey); + mCloneInfo = Move(aValue); + + mHaveValue = !mKey.IsUnset(); +} + +void +IDBCursor::Reset(Key&& aKey, + Key&& aSortKey, + Key&& aPrimaryKey) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mType == Type_IndexKey); + + Reset(); + + mKey = Move(aKey); + mSortKey = Move(aSortKey); + mPrimaryKey = Move(aPrimaryKey); + + mHaveValue = !mKey.IsUnset(); +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBCursor) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBCursor) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBCursor) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBCursor) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBCursor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceObjectStore) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceIndex) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBCursor) + MOZ_ASSERT_IF(!tmp->mHaveCachedKey, tmp->mCachedKey.isUndefined()); + MOZ_ASSERT_IF(!tmp->mHaveCachedPrimaryKey, + tmp->mCachedPrimaryKey.isUndefined()); + MOZ_ASSERT_IF(!tmp->mHaveCachedValue, tmp->mCachedValue.isUndefined()); + + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScriptOwner) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedKey) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedPrimaryKey) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedValue) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBCursor) + // Don't unlink mRequest, mSourceObjectStore, or mSourceIndex! + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + tmp->DropJSObjects(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +JSObject* +IDBCursor::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnOwningThread(); + + switch (mType) { + case Type_ObjectStore: + case Type_Index: + return IDBCursorWithValueBinding::Wrap(aCx, this, aGivenProto); + + case Type_ObjectStoreKey: + case Type_IndexKey: + return IDBCursorBinding::Wrap(aCx, this, aGivenProto); + + default: + MOZ_CRASH("Bad type!"); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBCursor.h b/dom/indexedDB/IDBCursor.h new file mode 100644 index 000000000..25be16bee --- /dev/null +++ b/dom/indexedDB/IDBCursor.h @@ -0,0 +1,224 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbcursor_h__ +#define mozilla_dom_idbcursor_h__ + +#include "IndexedDatabase.h" +#include "js/RootingAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class IDBIndex; +class IDBObjectStore; +class IDBRequest; +class IDBTransaction; +class OwningIDBObjectStoreOrIDBIndex; + +namespace indexedDB { +class BackgroundCursorChild; +} + +class IDBCursor final + : public nsISupports + , public nsWrapperCache +{ +public: + typedef indexedDB::Key Key; + typedef indexedDB::StructuredCloneReadInfo StructuredCloneReadInfo; + + enum Direction + { + NEXT = 0, + NEXT_UNIQUE, + PREV, + PREV_UNIQUE, + + // Only needed for IPC serialization helper, should never be used in code. + DIRECTION_INVALID + }; + +private: + enum Type + { + Type_ObjectStore, + Type_ObjectStoreKey, + Type_Index, + Type_IndexKey, + }; + + indexedDB::BackgroundCursorChild* mBackgroundActor; + + RefPtr<IDBRequest> mRequest; + RefPtr<IDBObjectStore> mSourceObjectStore; + RefPtr<IDBIndex> mSourceIndex; + + // mSourceObjectStore or mSourceIndex will hold this alive. + IDBTransaction* mTransaction; + + JS::Heap<JSObject*> mScriptOwner; + + // These are cycle-collected! + JS::Heap<JS::Value> mCachedKey; + JS::Heap<JS::Value> mCachedPrimaryKey; + JS::Heap<JS::Value> mCachedValue; + + Key mKey; + Key mSortKey; + Key mPrimaryKey; + StructuredCloneReadInfo mCloneInfo; + + const Type mType; + const Direction mDirection; + + bool mHaveCachedKey : 1; + bool mHaveCachedPrimaryKey : 1; + bool mHaveCachedValue : 1; + bool mRooted : 1; + bool mContinueCalled : 1; + bool mHaveValue : 1; + +public: + static already_AddRefed<IDBCursor> + Create(indexedDB::BackgroundCursorChild* aBackgroundActor, + const Key& aKey, + StructuredCloneReadInfo&& aCloneInfo); + + static already_AddRefed<IDBCursor> + Create(indexedDB::BackgroundCursorChild* aBackgroundActor, + const Key& aKey); + + static already_AddRefed<IDBCursor> + Create(indexedDB::BackgroundCursorChild* aBackgroundActor, + const Key& aKey, + const Key& aSortKey, + const Key& aPrimaryKey, + StructuredCloneReadInfo&& aCloneInfo); + + static already_AddRefed<IDBCursor> + Create(indexedDB::BackgroundCursorChild* aBackgroundActor, + const Key& aKey, + const Key& aSortKey, + const Key& aPrimaryKey); + + static Direction + ConvertDirection(IDBCursorDirection aDirection); + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + nsPIDOMWindowInner* + GetParentObject() const; + + void + GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const; + + IDBCursorDirection + GetDirection() const; + + void + GetKey(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + void + GetPrimaryKey(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + void + GetValue(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + void + Continue(JSContext* aCx, JS::Handle<JS::Value> aKey, ErrorResult& aRv); + + void + ContinuePrimaryKey(JSContext* aCx, + JS::Handle<JS::Value> aKey, + JS::Handle<JS::Value> aPrimaryKey, + ErrorResult& aRv); + + void + Advance(uint32_t aCount, ErrorResult& aRv); + + already_AddRefed<IDBRequest> + Update(JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv); + + already_AddRefed<IDBRequest> + Delete(JSContext* aCx, ErrorResult& aRv); + + void + Reset(); + + void + Reset(Key&& aKey, StructuredCloneReadInfo&& aValue); + + void + Reset(Key&& aKey); + + void + Reset(Key&& aKey, Key&& aSortKey, Key&& aPrimaryKey, StructuredCloneReadInfo&& aValue); + + void + Reset(Key&& aKey, Key&& aSortKey, Key&& aPrimaryKey); + + void + ClearBackgroundActor() + { + AssertIsOnOwningThread(); + + mBackgroundActor = nullptr; + } + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBCursor) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBCursor(Type aType, + indexedDB::BackgroundCursorChild* aBackgroundActor, + const Key& aKey); + + ~IDBCursor(); + +#ifdef ENABLE_INTL_API + // Checks if this is a locale aware cursor (ie. the index's sortKey is unset) + bool + IsLocaleAware() const; +#endif + + void + DropJSObjects(); + + bool + IsSourceDeleted() const; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbcursor_h__ diff --git a/dom/indexedDB/IDBDatabase.cpp b/dom/indexedDB/IDBDatabase.cpp new file mode 100644 index 000000000..5592e7f93 --- /dev/null +++ b/dom/indexedDB/IDBDatabase.cpp @@ -0,0 +1,1435 @@ +/* -*- 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 "IDBDatabase.h" + +#include "FileInfo.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBMutableFile.h" +#include "IDBObjectStore.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IDBFactory.h" +#include "IndexedDatabaseManager.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventDispatcher.h" +#include "MainThreadUtils.h" +#include "mozilla/Services.h" +#include "mozilla/storage.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/DOMStringListBinding.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IDBDatabaseBinding.h" +#include "mozilla/dom/IDBObjectStoreBinding.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/dom/ipc/BlobChild.h" +#include "mozilla/dom/ipc/nsIRemoteBlob.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/FileDescriptor.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsIDocument.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsISupportsPrimitives.h" +#include "nsThreadUtils.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "ScriptErrorHelper.h" +#include "nsQueryObject.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; +using namespace mozilla::services; + +namespace { + +const char kCycleCollectionObserverTopic[] = "cycle-collector-end"; +const char kMemoryPressureObserverTopic[] = "memory-pressure"; +const char kWindowObserverTopic[] = "inner-window-destroyed"; + +class CancelableRunnableWrapper final + : public CancelableRunnable +{ + nsCOMPtr<nsIRunnable> mRunnable; + +public: + explicit + CancelableRunnableWrapper(nsIRunnable* aRunnable) + : mRunnable(aRunnable) + { + MOZ_ASSERT(aRunnable); + } + +private: + ~CancelableRunnableWrapper() + { } + + NS_DECL_NSIRUNNABLE + nsresult Cancel() override; +}; + +class DatabaseFile final + : public PBackgroundIDBDatabaseFileChild +{ + IDBDatabase* mDatabase; + +public: + explicit DatabaseFile(IDBDatabase* aDatabase) + : mDatabase(aDatabase) + { + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(DatabaseFile); + } + +private: + ~DatabaseFile() + { + MOZ_ASSERT(!mDatabase); + + MOZ_COUNT_DTOR(DatabaseFile); + } + + virtual void + ActorDestroy(ActorDestroyReason aWhy) override + { + MOZ_ASSERT(mDatabase); + mDatabase->AssertIsOnOwningThread(); + + if (aWhy != Deletion) { + RefPtr<IDBDatabase> database = mDatabase; + database->NoteFinishedFileActor(this); + } + +#ifdef DEBUG + mDatabase = nullptr; +#endif + } +}; + +} // namespace + +class IDBDatabase::Observer final + : public nsIObserver +{ + IDBDatabase* mWeakDatabase; + const uint64_t mWindowId; + +public: + Observer(IDBDatabase* aDatabase, uint64_t aWindowId) + : mWeakDatabase(aDatabase) + , mWindowId(aWindowId) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDatabase); + } + + void + Revoke() + { + MOZ_ASSERT(NS_IsMainThread()); + + mWeakDatabase = nullptr; + } + + NS_DECL_ISUPPORTS + +private: + ~Observer() + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mWeakDatabase); + } + + NS_DECL_NSIOBSERVER +}; + +IDBDatabase::IDBDatabase(IDBOpenDBRequest* aRequest, + IDBFactory* aFactory, + BackgroundDatabaseChild* aActor, + DatabaseSpec* aSpec) + : IDBWrapperCache(aRequest) + , mFactory(aFactory) + , mSpec(aSpec) + , mBackgroundActor(aActor) + , mFileHandleDisabled(aRequest->IsFileHandleDisabled()) + , mClosed(false) + , mInvalidated(false) + , mQuotaExceeded(false) +{ + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aSpec); +} + +IDBDatabase::~IDBDatabase() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mBackgroundActor); +} + +// static +already_AddRefed<IDBDatabase> +IDBDatabase::Create(IDBOpenDBRequest* aRequest, + IDBFactory* aFactory, + BackgroundDatabaseChild* aActor, + DatabaseSpec* aSpec) +{ + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aSpec); + + RefPtr<IDBDatabase> db = + new IDBDatabase(aRequest, aFactory, aActor, aSpec); + + db->SetScriptOwner(aRequest->GetScriptOwner()); + + if (NS_IsMainThread()) { + if (nsPIDOMWindowInner* window = aFactory->GetParentObject()) { + uint64_t windowId = window->WindowID(); + + RefPtr<Observer> observer = new Observer(db, windowId); + + nsCOMPtr<nsIObserverService> obsSvc = GetObserverService(); + MOZ_ASSERT(obsSvc); + + // This topic must be successfully registered. + if (NS_WARN_IF(NS_FAILED( + obsSvc->AddObserver(observer, kWindowObserverTopic, false)))) { + observer->Revoke(); + return nullptr; + } + + // These topics are not crucial. + if (NS_FAILED(obsSvc->AddObserver(observer, + kCycleCollectionObserverTopic, + false)) || + NS_FAILED(obsSvc->AddObserver(observer, + kMemoryPressureObserverTopic, + false))) { + NS_WARNING("Failed to add additional memory observers!"); + } + + db->mObserver.swap(observer); + } + } + + return db.forget(); +} + +#ifdef DEBUG + +void +IDBDatabase::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mFactory); + mFactory->AssertIsOnOwningThread(); +} + +PRThread* +IDBDatabase::OwningThread() const +{ + MOZ_ASSERT(mFactory); + return mFactory->OwningThread(); +} + +#endif // DEBUG + +void +IDBDatabase::CloseInternal() +{ + AssertIsOnOwningThread(); + + if (!mClosed) { + mClosed = true; + + ExpireFileActors(/* aExpireAll */ true); + + if (mObserver) { + mObserver->Revoke(); + + nsCOMPtr<nsIObserverService> obsSvc = GetObserverService(); + if (obsSvc) { + // These might not have been registered. + obsSvc->RemoveObserver(mObserver, kCycleCollectionObserverTopic); + obsSvc->RemoveObserver(mObserver, kMemoryPressureObserverTopic); + + MOZ_ALWAYS_SUCCEEDS( + obsSvc->RemoveObserver(mObserver, kWindowObserverTopic)); + } + + mObserver = nullptr; + } + + if (mBackgroundActor && !mInvalidated) { + mBackgroundActor->SendClose(); + } + } +} + +void +IDBDatabase::InvalidateInternal() +{ + AssertIsOnOwningThread(); + + InvalidateMutableFiles(); + AbortTransactions(/* aShouldWarn */ true); + + CloseInternal(); +} + +void +IDBDatabase::EnterSetVersionTransaction(uint64_t aNewVersion) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aNewVersion); + MOZ_ASSERT(!RunningVersionChangeTransaction()); + MOZ_ASSERT(mSpec); + MOZ_ASSERT(!mPreviousSpec); + + mPreviousSpec = new DatabaseSpec(*mSpec); + + mSpec->metadata().version() = aNewVersion; +} + +void +IDBDatabase::ExitSetVersionTransaction() +{ + AssertIsOnOwningThread(); + + if (mPreviousSpec) { + mPreviousSpec = nullptr; + } +} + +void +IDBDatabase::RevertToPreviousState() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(RunningVersionChangeTransaction()); + MOZ_ASSERT(mPreviousSpec); + + // Hold the current spec alive until RefreshTransactionsSpecEnumerator has + // finished! + nsAutoPtr<DatabaseSpec> currentSpec(mSpec.forget()); + + mSpec = mPreviousSpec.forget(); + + RefreshSpec(/* aMayDelete */ true); +} + +void +IDBDatabase::RefreshSpec(bool aMayDelete) +{ + AssertIsOnOwningThread(); + + for (auto iter = mTransactions.Iter(); !iter.Done(); iter.Next()) { + RefPtr<IDBTransaction> transaction = iter.Get()->GetKey(); + MOZ_ASSERT(transaction); + transaction->AssertIsOnOwningThread(); + transaction->RefreshSpec(aMayDelete); + } +} + +nsPIDOMWindowInner* +IDBDatabase::GetParentObject() const +{ + return mFactory->GetParentObject(); +} + +const nsString& +IDBDatabase::Name() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return mSpec->metadata().name(); +} + +uint64_t +IDBDatabase::Version() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return mSpec->metadata().version(); +} + +already_AddRefed<DOMStringList> +IDBDatabase::ObjectStoreNames() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + const nsTArray<ObjectStoreSpec>& objectStores = mSpec->objectStores(); + + RefPtr<DOMStringList> list = new DOMStringList(); + + if (!objectStores.IsEmpty()) { + nsTArray<nsString>& listNames = list->StringArray(); + listNames.SetCapacity(objectStores.Length()); + + for (uint32_t index = 0; index < objectStores.Length(); index++) { + listNames.InsertElementSorted(objectStores[index].metadata().name()); + } + } + + return list.forget(); +} + +already_AddRefed<nsIDocument> +IDBDatabase::GetOwnerDocument() const +{ + if (nsPIDOMWindowInner* window = GetOwner()) { + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + return doc.forget(); + } + return nullptr; +} + +already_AddRefed<IDBObjectStore> +IDBDatabase::CreateObjectStore( + const nsAString& aName, + const IDBObjectStoreParameters& aOptionalParameters, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + IDBTransaction* transaction = IDBTransaction::GetCurrent(); + if (!transaction || + transaction->Database() != this || + transaction->GetMode() != IDBTransaction::VERSION_CHANGE) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + KeyPath keyPath(0); + if (NS_FAILED(KeyPath::Parse(aOptionalParameters.mKeyPath, &keyPath))) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + + nsTArray<ObjectStoreSpec>& objectStores = mSpec->objectStores(); + for (uint32_t count = objectStores.Length(), index = 0; + index < count; + index++) { + if (aName == objectStores[index].metadata().name()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR); + return nullptr; + } + } + + if (!keyPath.IsAllowedForObjectStore(aOptionalParameters.mAutoIncrement)) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + + const ObjectStoreSpec* oldSpecElements = + objectStores.IsEmpty() ? nullptr : objectStores.Elements(); + + ObjectStoreSpec* newSpec = objectStores.AppendElement(); + newSpec->metadata() = + ObjectStoreMetadata(transaction->NextObjectStoreId(), nsString(aName), + keyPath, aOptionalParameters.mAutoIncrement); + + if (oldSpecElements && + oldSpecElements != objectStores.Elements()) { + MOZ_ASSERT(objectStores.Length() > 1); + + // Array got moved, update the spec pointers for all live objectStores and + // indexes. + RefreshSpec(/* aMayDelete */ false); + } + + RefPtr<IDBObjectStore> objectStore = + transaction->CreateObjectStore(*newSpec); + MOZ_ASSERT(objectStore); + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).createObjectStore(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: " + "IDBDatabase.createObjectStore()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(objectStore)); + + return objectStore.forget(); +} + +void +IDBDatabase::DeleteObjectStore(const nsAString& aName, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + IDBTransaction* transaction = IDBTransaction::GetCurrent(); + if (!transaction || + transaction->Database() != this || + transaction->GetMode() != IDBTransaction::VERSION_CHANGE) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + nsTArray<ObjectStoreSpec>& specArray = mSpec->objectStores(); + + int64_t objectStoreId = 0; + + for (uint32_t specCount = specArray.Length(), specIndex = 0; + specIndex < specCount; + specIndex++) { + const ObjectStoreMetadata& metadata = specArray[specIndex].metadata(); + MOZ_ASSERT(metadata.id()); + + if (aName == metadata.name()) { + objectStoreId = metadata.id(); + + // Must do this before altering the metadata array! + transaction->DeleteObjectStore(objectStoreId); + + specArray.RemoveElementAt(specIndex); + + RefreshSpec(/* aMayDelete */ false); + break; + } + } + + if (!objectStoreId) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return; + } + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).deleteObjectStore(\"%s\")", + "IndexedDB %s: C T[%lld] R[%llu]: " + "IDBDatabase.deleteObjectStore()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(transaction), + NS_ConvertUTF16toUTF8(aName).get()); +} + +already_AddRefed<IDBTransaction> +IDBDatabase::Transaction(JSContext* aCx, + const StringOrStringSequence& aStoreNames, + IDBTransactionMode aMode, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if ((aMode == IDBTransactionMode::Readwriteflush || + aMode == IDBTransactionMode::Cleanup) && + !IndexedDatabaseManager::ExperimentalFeaturesEnabled()) { + // Pretend that this mode doesn't exist. We don't have a way to annotate + // certain enum values as depending on preferences so we just duplicate the + // normal exception generation here. + aRv.ThrowTypeError<MSG_INVALID_ENUM_VALUE>( + NS_LITERAL_STRING("Argument 2 of IDBDatabase.transaction"), + NS_LITERAL_STRING("readwriteflush"), + NS_LITERAL_STRING("IDBTransactionMode")); + return nullptr; + } + + RefPtr<IDBTransaction> transaction; + aRv = Transaction(aCx, aStoreNames, aMode, getter_AddRefs(transaction)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return transaction.forget(); +} + +nsresult +IDBDatabase::Transaction(JSContext* aCx, + const StringOrStringSequence& aStoreNames, + IDBTransactionMode aMode, + IDBTransaction** aTransaction) +{ + AssertIsOnOwningThread(); + + if (NS_WARN_IF((aMode == IDBTransactionMode::Readwriteflush || + aMode == IDBTransactionMode::Cleanup) && + !IndexedDatabaseManager::ExperimentalFeaturesEnabled())) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (QuotaManager::IsShuttingDown()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (mClosed || RunningVersionChangeTransaction()) { + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + AutoTArray<nsString, 1> stackSequence; + + if (aStoreNames.IsString()) { + stackSequence.AppendElement(aStoreNames.GetAsString()); + } else { + MOZ_ASSERT(aStoreNames.IsStringSequence()); + if (aStoreNames.GetAsStringSequence().IsEmpty()) { + return NS_ERROR_DOM_INVALID_ACCESS_ERR; + } + } + + const nsTArray<nsString>& storeNames = + aStoreNames.IsString() ? + stackSequence : + static_cast<const nsTArray<nsString>&>(aStoreNames.GetAsStringSequence()); + MOZ_ASSERT(!storeNames.IsEmpty()); + + const nsTArray<ObjectStoreSpec>& objectStores = mSpec->objectStores(); + const uint32_t nameCount = storeNames.Length(); + + nsTArray<nsString> sortedStoreNames; + sortedStoreNames.SetCapacity(nameCount); + + // Check to make sure the object store names we collected actually exist. + for (uint32_t nameIndex = 0; nameIndex < nameCount; nameIndex++) { + const nsString& name = storeNames[nameIndex]; + + bool found = false; + + for (uint32_t objCount = objectStores.Length(), objIndex = 0; + objIndex < objCount; + objIndex++) { + if (objectStores[objIndex].metadata().name() == name) { + found = true; + break; + } + } + + if (!found) { + return NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR; + } + + sortedStoreNames.InsertElementSorted(name); + } + + // Remove any duplicates. + for (uint32_t nameIndex = nameCount - 1; nameIndex > 0; nameIndex--) { + if (sortedStoreNames[nameIndex] == sortedStoreNames[nameIndex - 1]) { + sortedStoreNames.RemoveElementAt(nameIndex); + } + } + + IDBTransaction::Mode mode; + switch (aMode) { + case IDBTransactionMode::Readonly: + mode = IDBTransaction::READ_ONLY; + break; + case IDBTransactionMode::Readwrite: + if (mQuotaExceeded) { + mode = IDBTransaction::CLEANUP; + mQuotaExceeded = false; + } else { + mode = IDBTransaction::READ_WRITE; + } + break; + case IDBTransactionMode::Readwriteflush: + mode = IDBTransaction::READ_WRITE_FLUSH; + break; + case IDBTransactionMode::Cleanup: + mode = IDBTransaction::CLEANUP; + mQuotaExceeded = false; + break; + case IDBTransactionMode::Versionchange: + return NS_ERROR_DOM_TYPE_ERR; + + default: + MOZ_CRASH("Unknown mode!"); + } + + RefPtr<IDBTransaction> transaction = + IDBTransaction::Create(aCx, this, sortedStoreNames, mode); + if (NS_WARN_IF(!transaction)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + BackgroundTransactionChild* actor = + new BackgroundTransactionChild(transaction); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld]: " + "database(%s).transaction(%s)", + "IndexedDB %s: C T[%lld]: IDBDatabase.transaction()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(transaction)); + + MOZ_ALWAYS_TRUE( + mBackgroundActor->SendPBackgroundIDBTransactionConstructor(actor, + sortedStoreNames, + mode)); + + transaction->SetBackgroundActor(actor); + + if (mode == IDBTransaction::CLEANUP) { + ExpireFileActors(/* aExpireAll */ true); + } + + transaction.forget(aTransaction); + return NS_OK; +} + +StorageType +IDBDatabase::Storage() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return PersistenceTypeToStorage(mSpec->metadata().persistenceType()); +} + +already_AddRefed<IDBRequest> +IDBDatabase::CreateMutableFile(JSContext* aCx, + const nsAString& aName, + const Optional<nsAString>& aType, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (QuotaManager::IsShuttingDown()) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + if (mClosed || mFileHandleDisabled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + nsString type; + if (aType.WasPassed()) { + type = aType.Value(); + } + + CreateFileParams params(nsString(aName), type); + + RefPtr<IDBRequest> request = IDBRequest::Create(aCx, this, nullptr); + MOZ_ASSERT(request); + + BackgroundDatabaseRequestChild* actor = + new BackgroundDatabaseRequestChild(this, request); + + IDB_LOG_MARK("IndexedDB %s: Child Request[%llu]: " + "database(%s).createMutableFile(%s)", + "IndexedDB %s: C R[%llu]: IDBDatabase.createMutableFile()", + IDB_LOG_ID_STRING(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(this), + NS_ConvertUTF16toUTF8(aName).get()); + + mBackgroundActor->SendPBackgroundIDBDatabaseRequestConstructor(actor, params); + + return request.forget(); +} + +void +IDBDatabase::RegisterTransaction(IDBTransaction* aTransaction) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + MOZ_ASSERT(!mTransactions.Contains(aTransaction)); + + mTransactions.PutEntry(aTransaction); +} + +void +IDBDatabase::UnregisterTransaction(IDBTransaction* aTransaction) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + MOZ_ASSERT(mTransactions.Contains(aTransaction)); + + mTransactions.RemoveEntry(aTransaction); +} + +void +IDBDatabase::AbortTransactions(bool aShouldWarn) +{ + AssertIsOnOwningThread(); + + class MOZ_STACK_CLASS Helper final + { + typedef AutoTArray<RefPtr<IDBTransaction>, 20> StrongTransactionArray; + typedef AutoTArray<IDBTransaction*, 20> WeakTransactionArray; + + public: + static void + AbortTransactions(IDBDatabase* aDatabase, const bool aShouldWarn) + { + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + nsTHashtable<nsPtrHashKey<IDBTransaction>>& transactionTable = + aDatabase->mTransactions; + + if (!transactionTable.Count()) { + return; + } + + StrongTransactionArray transactionsToAbort; + transactionsToAbort.SetCapacity(transactionTable.Count()); + + for (auto iter = transactionTable.Iter(); !iter.Done(); iter.Next()) { + IDBTransaction* transaction = iter.Get()->GetKey(); + MOZ_ASSERT(transaction); + + transaction->AssertIsOnOwningThread(); + + // Transactions that are already done can simply be ignored. Otherwise + // there is a race here and it's possible that the transaction has not + // been successfully committed yet so we will warn the user. + if (!transaction->IsDone()) { + transactionsToAbort.AppendElement(transaction); + } + } + MOZ_ASSERT(transactionsToAbort.Length() <= transactionTable.Count()); + + if (transactionsToAbort.IsEmpty()) { + return; + } + + // We want to abort transactions as soon as possible so we iterate the + // transactions once and abort them all first, collecting the transactions + // that need to have a warning issued along the way. Those that need a + // warning will be a subset of those that are aborted, so we don't need + // additional strong references here. + WeakTransactionArray transactionsThatNeedWarning; + + for (RefPtr<IDBTransaction>& transaction : transactionsToAbort) { + MOZ_ASSERT(transaction); + MOZ_ASSERT(!transaction->IsDone()); + + if (aShouldWarn) { + switch (transaction->GetMode()) { + // We ignore transactions that could not have written any data. + case IDBTransaction::READ_ONLY: + break; + + // We warn for any transactions that could have written data. + case IDBTransaction::READ_WRITE: + case IDBTransaction::READ_WRITE_FLUSH: + case IDBTransaction::CLEANUP: + case IDBTransaction::VERSION_CHANGE: + transactionsThatNeedWarning.AppendElement(transaction); + break; + + default: + MOZ_CRASH("Unknown mode!"); + } + } + + transaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } + + static const char kWarningMessage[] = + "IndexedDBTransactionAbortNavigation"; + + for (IDBTransaction* transaction : transactionsThatNeedWarning) { + MOZ_ASSERT(transaction); + + nsString filename; + uint32_t lineNo, column; + transaction->GetCallerLocation(filename, &lineNo, &column); + + aDatabase->LogWarning(kWarningMessage, filename, lineNo, column); + } + } + }; + + Helper::AbortTransactions(this, aShouldWarn); +} + +PBackgroundIDBDatabaseFileChild* +IDBDatabase::GetOrCreateFileActorForBlob(Blob* aBlob) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aBlob); + MOZ_ASSERT(mBackgroundActor); + + // We use the File's nsIWeakReference as the key to the table because + // a) it is unique per blob, b) it is reference-counted so that we can + // guarantee that it stays alive, and c) it doesn't hold the actual File + // alive. + nsCOMPtr<nsIDOMBlob> blob = aBlob; + nsCOMPtr<nsIWeakReference> weakRef = do_GetWeakReference(blob); + MOZ_ASSERT(weakRef); + + PBackgroundIDBDatabaseFileChild* actor = nullptr; + + if (!mFileActors.Get(weakRef, &actor)) { + BlobImpl* blobImpl = aBlob->Impl(); + MOZ_ASSERT(blobImpl); + + if (mReceivedBlobs.GetEntry(weakRef)) { + // This blob was previously retrieved from the database. + nsCOMPtr<nsIRemoteBlob> remoteBlob = do_QueryObject(blobImpl); + MOZ_ASSERT(remoteBlob); + + BlobChild* blobChild = remoteBlob->GetBlobChild(); + MOZ_ASSERT(blobChild); + +#ifdef DEBUG + { + PBackgroundChild* backgroundManager = blobChild->GetBackgroundManager(); + MOZ_ASSERT(backgroundManager); + + PBackgroundChild* thisManager = mBackgroundActor->Manager()->Manager(); + MOZ_ASSERT(thisManager); + + MOZ_ASSERT(thisManager == backgroundManager); + } +#endif + auto* dbFile = new DatabaseFile(this); + + actor = + mBackgroundActor->SendPBackgroundIDBDatabaseFileConstructor(dbFile, + blobChild); + if (NS_WARN_IF(!actor)) { + return nullptr; + } + } else { + // Make sure that the input stream we get here is one that can actually be + // serialized to PBackground. + PBackgroundChild* backgroundManager = + mBackgroundActor->Manager()->Manager(); + MOZ_ASSERT(backgroundManager); + + auto* blobChild = + static_cast<BlobChild*>( + BackgroundChild::GetOrCreateActorForBlob(backgroundManager, aBlob)); + MOZ_ASSERT(blobChild); + + auto* dbFile = new DatabaseFile(this); + + actor = + mBackgroundActor->SendPBackgroundIDBDatabaseFileConstructor(dbFile, + blobChild); + if (NS_WARN_IF(!actor)) { + return nullptr; + } + } + + MOZ_ASSERT(actor); + + mFileActors.Put(weakRef, actor); + } + + MOZ_ASSERT(actor); + + return actor; +} + +void +IDBDatabase::NoteFinishedFileActor(PBackgroundIDBDatabaseFileChild* aFileActor) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aFileActor); + + for (auto iter = mFileActors.Iter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(iter.Key()); + PBackgroundIDBDatabaseFileChild* actor = iter.Data(); + MOZ_ASSERT(actor); + + if (actor == aFileActor) { + iter.Remove(); + } + } +} + +void +IDBDatabase::NoteReceivedBlob(Blob* aBlob) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aBlob); + MOZ_ASSERT(mBackgroundActor); + +#ifdef DEBUG + { + RefPtr<BlobImpl> blobImpl = aBlob->Impl(); + MOZ_ASSERT(blobImpl); + + nsCOMPtr<nsIRemoteBlob> remoteBlob = do_QueryObject(blobImpl); + MOZ_ASSERT(remoteBlob); + + BlobChild* blobChild = remoteBlob->GetBlobChild(); + MOZ_ASSERT(blobChild); + + PBackgroundChild* backgroundManager = blobChild->GetBackgroundManager(); + MOZ_ASSERT(backgroundManager); + + PBackgroundChild* thisManager = mBackgroundActor->Manager()->Manager(); + MOZ_ASSERT(thisManager); + + MOZ_ASSERT(thisManager == backgroundManager); + } +#endif + + nsCOMPtr<nsIDOMBlob> blob = aBlob; + nsCOMPtr<nsIWeakReference> weakRef = do_GetWeakReference(blob); + MOZ_ASSERT(weakRef); + + // It's ok if this entry already exists in the table. + mReceivedBlobs.PutEntry(weakRef); +} + +void +IDBDatabase::DelayedMaybeExpireFileActors() +{ + AssertIsOnOwningThread(); + + if (!mBackgroundActor || !mFileActors.Count()) { + return; + } + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod<bool>(this, + &IDBDatabase::ExpireFileActors, + /* aExpireAll */ false); + MOZ_ASSERT(runnable); + + if (!NS_IsMainThread()) { + // Wrap as a nsICancelableRunnable to make workers happy. + nsCOMPtr<nsIRunnable> cancelable = new CancelableRunnableWrapper(runnable); + cancelable.swap(runnable); + } + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable)); +} + +nsresult +IDBDatabase::GetQuotaInfo(nsACString& aOrigin, + PersistenceType* aPersistenceType) +{ + using mozilla::dom::quota::QuotaManager; + + MOZ_ASSERT(NS_IsMainThread(), "This can't work off the main thread!"); + + if (aPersistenceType) { + *aPersistenceType = mSpec->metadata().persistenceType(); + MOZ_ASSERT(*aPersistenceType != PERSISTENCE_TYPE_INVALID); + } + + PrincipalInfo* principalInfo = mFactory->GetPrincipalInfo(); + MOZ_ASSERT(principalInfo); + + switch (principalInfo->type()) { + case PrincipalInfo::TNullPrincipalInfo: + MOZ_CRASH("Is this needed?!"); + + case PrincipalInfo::TSystemPrincipalInfo: + QuotaManager::GetInfoForChrome(nullptr, nullptr, &aOrigin, nullptr); + return NS_OK; + + case PrincipalInfo::TContentPrincipalInfo: { + nsresult rv; + nsCOMPtr<nsIPrincipal> principal = + PrincipalInfoToPrincipal(*principalInfo, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = QuotaManager::GetInfoFromPrincipal(principal, + nullptr, + nullptr, + &aOrigin, + nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + default: + MOZ_CRASH("Unknown PrincipalInfo type!"); + } + + MOZ_CRASH("Should never get here!"); +} + +void +IDBDatabase::ExpireFileActors(bool aExpireAll) +{ + AssertIsOnOwningThread(); + + if (mBackgroundActor && mFileActors.Count()) { + for (auto iter = mFileActors.Iter(); !iter.Done(); iter.Next()) { + nsISupports* key = iter.Key(); + PBackgroundIDBDatabaseFileChild* actor = iter.Data(); + MOZ_ASSERT(key); + MOZ_ASSERT(actor); + + bool shouldExpire = aExpireAll; + if (!shouldExpire) { + nsCOMPtr<nsIWeakReference> weakRef = do_QueryInterface(key); + MOZ_ASSERT(weakRef); + + nsCOMPtr<nsISupports> referent = do_QueryReferent(weakRef); + shouldExpire = !referent; + } + + if (shouldExpire) { + PBackgroundIDBDatabaseFileChild::Send__delete__(actor); + + if (!aExpireAll) { + iter.Remove(); + } + } + } + if (aExpireAll) { + mFileActors.Clear(); + } + } else { + MOZ_ASSERT(!mFileActors.Count()); + } + + if (mReceivedBlobs.Count()) { + if (aExpireAll) { + mReceivedBlobs.Clear(); + } else { + for (auto iter = mReceivedBlobs.Iter(); !iter.Done(); iter.Next()) { + nsISupports* key = iter.Get()->GetKey(); + MOZ_ASSERT(key); + + nsCOMPtr<nsIWeakReference> weakRef = do_QueryInterface(key); + MOZ_ASSERT(weakRef); + + nsCOMPtr<nsISupports> referent = do_QueryReferent(weakRef); + if (!referent) { + iter.Remove(); + } + } + } + } +} + +void +IDBDatabase::NoteLiveMutableFile(IDBMutableFile* aMutableFile) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aMutableFile); + aMutableFile->AssertIsOnOwningThread(); + MOZ_ASSERT(!mLiveMutableFiles.Contains(aMutableFile)); + + mLiveMutableFiles.AppendElement(aMutableFile); +} + +void +IDBDatabase::NoteFinishedMutableFile(IDBMutableFile* aMutableFile) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aMutableFile); + aMutableFile->AssertIsOnOwningThread(); + + // It's ok if this is called after we cleared the array, so don't assert that + // aMutableFile is in the list. + + mLiveMutableFiles.RemoveElement(aMutableFile); +} + +void +IDBDatabase::InvalidateMutableFiles() +{ + AssertIsOnOwningThread(); + + if (!mLiveMutableFiles.IsEmpty()) { + for (uint32_t count = mLiveMutableFiles.Length(), index = 0; + index < count; + index++) { + mLiveMutableFiles[index]->Invalidate(); + } + + mLiveMutableFiles.Clear(); + } +} + +void +IDBDatabase::Invalidate() +{ + AssertIsOnOwningThread(); + + if (!mInvalidated) { + mInvalidated = true; + + InvalidateInternal(); + } +} + +void +IDBDatabase::LogWarning(const char* aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aMessageName); + + ScriptErrorHelper::DumpLocalizedMessage(nsDependentCString(aMessageName), + aFilename, + aLineNumber, + aColumnNumber, + nsIScriptError::warningFlag, + mFactory->IsChrome(), + mFactory->InnerWindowID()); +} + +NS_IMPL_ADDREF_INHERITED(IDBDatabase, IDBWrapperCache) +NS_IMPL_RELEASE_INHERITED(IDBDatabase, IDBWrapperCache) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBDatabase) +NS_INTERFACE_MAP_END_INHERITING(IDBWrapperCache) + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBDatabase) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBDatabase, IDBWrapperCache) + tmp->AssertIsOnOwningThread(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFactory) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBDatabase, IDBWrapperCache) + tmp->AssertIsOnOwningThread(); + + // Don't unlink mFactory! + + // We've been unlinked, at the very least we should be able to prevent further + // transactions from starting and unblock any other SetVersion callers. + tmp->CloseInternal(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +void +IDBDatabase::LastRelease() +{ + AssertIsOnOwningThread(); + + CloseInternal(); + + if (mBackgroundActor) { + mBackgroundActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } +} + +nsresult +IDBDatabase::PostHandleEvent(EventChainPostVisitor& aVisitor) +{ + nsresult rv = + IndexedDatabaseManager::CommonPostHandleEvent(aVisitor, mFactory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +JSObject* +IDBDatabase::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBDatabaseBinding::Wrap(aCx, this, aGivenProto); +} + +NS_IMETHODIMP +CancelableRunnableWrapper::Run() +{ + nsCOMPtr<nsIRunnable> runnable; + mRunnable.swap(runnable); + + if (runnable) { + return runnable->Run(); + } + + return NS_OK; +} + +nsresult +CancelableRunnableWrapper::Cancel() +{ + if (mRunnable) { + mRunnable = nullptr; + return NS_OK; + } + + return NS_ERROR_UNEXPECTED; +} + +NS_IMPL_ISUPPORTS(IDBDatabase::Observer, nsIObserver) + +NS_IMETHODIMP +IDBDatabase:: +Observer::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTopic); + + if (!strcmp(aTopic, kWindowObserverTopic)) { + if (mWeakDatabase) { + nsCOMPtr<nsISupportsPRUint64> supportsInt = do_QueryInterface(aSubject); + MOZ_ASSERT(supportsInt); + + uint64_t windowId; + MOZ_ALWAYS_SUCCEEDS(supportsInt->GetData(&windowId)); + + if (windowId == mWindowId) { + RefPtr<IDBDatabase> database = mWeakDatabase; + mWeakDatabase = nullptr; + + database->InvalidateInternal(); + } + } + + return NS_OK; + } + + if (!strcmp(aTopic, kCycleCollectionObserverTopic) || + !strcmp(aTopic, kMemoryPressureObserverTopic)) { + if (mWeakDatabase) { + RefPtr<IDBDatabase> database = mWeakDatabase; + + database->ExpireFileActors(/* aExpireAll */ false); + } + + return NS_OK; + } + + NS_WARNING("Unknown observer topic!"); + return NS_OK; +} + +nsresult +IDBDatabase::RenameObjectStore(int64_t aObjectStoreId, const nsAString& aName) +{ + MOZ_ASSERT(mSpec); + + nsTArray<ObjectStoreSpec>& objectStores = mSpec->objectStores(); + + ObjectStoreSpec* foundObjectStoreSpec = nullptr; + // Find the matched object store spec and check if 'aName' is already used by + // another object store. + for (uint32_t objCount = objectStores.Length(), objIndex = 0; + objIndex < objCount; + objIndex++) { + const ObjectStoreSpec& objSpec = objectStores[objIndex]; + if (objSpec.metadata().id() == aObjectStoreId) { + MOZ_ASSERT(!foundObjectStoreSpec); + foundObjectStoreSpec = &objectStores[objIndex]; + continue; + } + if (aName == objSpec.metadata().name()) { + return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + } + } + + MOZ_ASSERT(foundObjectStoreSpec); + + // Update the name of the matched object store. + foundObjectStoreSpec->metadata().name() = nsString(aName); + + return NS_OK; +} + +nsresult +IDBDatabase::RenameIndex(int64_t aObjectStoreId, + int64_t aIndexId, + const nsAString& aName) +{ + MOZ_ASSERT(mSpec); + + nsTArray<ObjectStoreSpec>& objectStores = mSpec->objectStores(); + + ObjectStoreSpec* foundObjectStoreSpec = nullptr; + // Find the matched index metadata and check if 'aName' is already used by + // another index. + for (uint32_t objCount = objectStores.Length(), objIndex = 0; + objIndex < objCount; + objIndex++) { + const ObjectStoreSpec& objSpec = objectStores[objIndex]; + if (objSpec.metadata().id() == aObjectStoreId) { + foundObjectStoreSpec = &objectStores[objIndex]; + break; + } + } + + MOZ_ASSERT(foundObjectStoreSpec); + + nsTArray<IndexMetadata>& indexes = foundObjectStoreSpec->indexes(); + IndexMetadata* foundIndexMetadata = nullptr; + for (uint32_t idxCount = indexes.Length(), idxIndex = 0; + idxIndex < idxCount; + idxIndex++) { + const IndexMetadata& metadata = indexes[idxIndex]; + if (metadata.id() == aIndexId) { + MOZ_ASSERT(!foundIndexMetadata); + foundIndexMetadata = &indexes[idxIndex]; + continue; + } + if (aName == metadata.name()) { + return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + } + } + + MOZ_ASSERT(foundIndexMetadata); + + // Update the name of the matched object store. + foundIndexMetadata->name() = nsString(aName); + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBDatabase.h b/dom/indexedDB/IDBDatabase.h new file mode 100644 index 000000000..e61d5a30d --- /dev/null +++ b/dom/indexedDB/IDBDatabase.h @@ -0,0 +1,347 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbdatabase_h__ +#define mozilla_dom_idbdatabase_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBTransactionBinding.h" +#include "mozilla/dom/StorageTypeBinding.h" +#include "mozilla/dom/IDBWrapperCache.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "nsAutoPtr.h" +#include "nsDataHashtable.h" +#include "nsHashKeys.h" +#include "nsString.h" +#include "nsTHashtable.h" + +class nsIDocument; +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; +class EventChainPostVisitor; + +namespace dom { + +class Blob; +class DOMStringList; +class IDBFactory; +class IDBMutableFile; +class IDBObjectStore; +struct IDBObjectStoreParameters; +class IDBOpenDBRequest; +class IDBRequest; +class IDBTransaction; +template <class> class Optional; +class StringOrStringSequence; + +namespace indexedDB { +class BackgroundDatabaseChild; +class DatabaseSpec; +class PBackgroundIDBDatabaseFileChild; +} + +class IDBDatabase final + : public IDBWrapperCache +{ + typedef mozilla::dom::indexedDB::DatabaseSpec DatabaseSpec; + typedef mozilla::dom::StorageType StorageType; + typedef mozilla::dom::quota::PersistenceType PersistenceType; + + class Observer; + friend class Observer; + + friend class IDBObjectStore; + friend class IDBIndex; + + // The factory must be kept alive when IndexedDB is used in multiple + // processes. If it dies then the entire actor tree will be destroyed with it + // and the world will explode. + RefPtr<IDBFactory> mFactory; + + nsAutoPtr<DatabaseSpec> mSpec; + + // Normally null except during a versionchange transaction. + nsAutoPtr<DatabaseSpec> mPreviousSpec; + + indexedDB::BackgroundDatabaseChild* mBackgroundActor; + + nsTHashtable<nsPtrHashKey<IDBTransaction>> mTransactions; + + nsDataHashtable<nsISupportsHashKey, indexedDB::PBackgroundIDBDatabaseFileChild*> + mFileActors; + + nsTHashtable<nsISupportsHashKey> mReceivedBlobs; + + RefPtr<Observer> mObserver; + + // Weak refs, IDBMutableFile strongly owns this IDBDatabase object. + nsTArray<IDBMutableFile*> mLiveMutableFiles; + + const bool mFileHandleDisabled; + bool mClosed; + bool mInvalidated; + bool mQuotaExceeded; + +public: + static already_AddRefed<IDBDatabase> + Create(IDBOpenDBRequest* aRequest, + IDBFactory* aFactory, + indexedDB::BackgroundDatabaseChild* aActor, + DatabaseSpec* aSpec); + +#ifdef DEBUG + void + AssertIsOnOwningThread() const; + + PRThread* + OwningThread() const; +#else + void + AssertIsOnOwningThread() const + { } +#endif + + const nsString& + Name() const; + + void + GetName(nsAString& aName) const + { + AssertIsOnOwningThread(); + + aName = Name(); + } + + uint64_t + Version() const; + + already_AddRefed<nsIDocument> + GetOwnerDocument() const; + + void + Close() + { + AssertIsOnOwningThread(); + + CloseInternal(); + } + + bool + IsClosed() const + { + AssertIsOnOwningThread(); + + return mClosed; + } + + void + Invalidate(); + + // Whether or not the database has been invalidated. If it has then no further + // transactions for this database will be allowed to run. + bool + IsInvalidated() const + { + AssertIsOnOwningThread(); + + return mInvalidated; + } + + void + SetQuotaExceeded() + { + mQuotaExceeded = true; + } + + void + EnterSetVersionTransaction(uint64_t aNewVersion); + + void + ExitSetVersionTransaction(); + + // Called when a versionchange transaction is aborted to reset the + // DatabaseInfo. + void + RevertToPreviousState(); + + IDBFactory* + Factory() const + { + AssertIsOnOwningThread(); + + return mFactory; + } + + void + RegisterTransaction(IDBTransaction* aTransaction); + + void + UnregisterTransaction(IDBTransaction* aTransaction); + + void + AbortTransactions(bool aShouldWarn); + + indexedDB::PBackgroundIDBDatabaseFileChild* + GetOrCreateFileActorForBlob(Blob* aBlob); + + void + NoteFinishedFileActor(indexedDB::PBackgroundIDBDatabaseFileChild* aFileActor); + + void + NoteReceivedBlob(Blob* aBlob); + + void + DelayedMaybeExpireFileActors(); + + // XXX This doesn't really belong here... It's only needed for IDBMutableFile + // serialization and should be removed or fixed someday. + nsresult + GetQuotaInfo(nsACString& aOrigin, PersistenceType* aPersistenceType); + + bool + IsFileHandleDisabled() const + { + return mFileHandleDisabled; + } + + void + NoteLiveMutableFile(IDBMutableFile* aMutableFile); + + void + NoteFinishedMutableFile(IDBMutableFile* aMutableFile); + + nsPIDOMWindowInner* + GetParentObject() const; + + already_AddRefed<DOMStringList> + ObjectStoreNames() const; + + already_AddRefed<IDBObjectStore> + CreateObjectStore(const nsAString& aName, + const IDBObjectStoreParameters& aOptionalParameters, + ErrorResult& aRv); + + void + DeleteObjectStore(const nsAString& name, ErrorResult& aRv); + + // This will be called from the DOM. + already_AddRefed<IDBTransaction> + Transaction(JSContext* aCx, + const StringOrStringSequence& aStoreNames, + IDBTransactionMode aMode, + ErrorResult& aRv); + + // This can be called from C++ to avoid JS exception. + nsresult + Transaction(JSContext* aCx, + const StringOrStringSequence& aStoreNames, + IDBTransactionMode aMode, + IDBTransaction** aTransaction); + + StorageType + Storage() const; + + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(close) + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(versionchange) + + already_AddRefed<IDBRequest> + CreateMutableFile(JSContext* aCx, + const nsAString& aName, + const Optional<nsAString>& aType, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + MozCreateFileHandle(JSContext* aCx, + const nsAString& aName, + const Optional<nsAString>& aType, + ErrorResult& aRv) + { + return CreateMutableFile(aCx, aName, aType, aRv); + } + + void + ClearBackgroundActor() + { + AssertIsOnOwningThread(); + + mBackgroundActor = nullptr; + } + + const DatabaseSpec* + Spec() const + { + return mSpec; + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBDatabase, IDBWrapperCache) + + // nsIDOMEventTarget + virtual void + LastRelease() override; + + virtual nsresult + PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBDatabase(IDBOpenDBRequest* aRequest, + IDBFactory* aFactory, + indexedDB::BackgroundDatabaseChild* aActor, + DatabaseSpec* aSpec); + + ~IDBDatabase(); + + void + CloseInternal(); + + void + InvalidateInternal(); + + bool + RunningVersionChangeTransaction() const + { + AssertIsOnOwningThread(); + + return !!mPreviousSpec; + } + + void + RefreshSpec(bool aMayDelete); + + void + ExpireFileActors(bool aExpireAll); + + void + InvalidateMutableFiles(); + + void + LogWarning(const char* aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber); + + // Only accessed by IDBObjectStore. + nsresult + RenameObjectStore(int64_t aObjectStoreId, const nsAString& aName); + + // Only accessed by IDBIndex. + nsresult + RenameIndex(int64_t aObjectStoreId, int64_t aIndexId, const nsAString& aName); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbdatabase_h__ diff --git a/dom/indexedDB/IDBEvents.cpp b/dom/indexedDB/IDBEvents.cpp new file mode 100644 index 000000000..bce567c51 --- /dev/null +++ b/dom/indexedDB/IDBEvents.cpp @@ -0,0 +1,98 @@ +/* -*- 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 "IDBEvents.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/IDBVersionChangeEventBinding.h" +#include "nsString.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::indexedDB; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +const char16_t* kAbortEventType = u"abort"; +const char16_t* kBlockedEventType = u"blocked"; +const char16_t* kCompleteEventType = u"complete"; +const char16_t* kErrorEventType = u"error"; +const char16_t* kSuccessEventType = u"success"; +const char16_t* kUpgradeNeededEventType = u"upgradeneeded"; +const char16_t* kVersionChangeEventType = u"versionchange"; +const char16_t* kCloseEventType = u"close"; + +already_AddRefed<nsIDOMEvent> +CreateGenericEvent(EventTarget* aOwner, + const nsDependentString& aType, + Bubbles aBubbles, + Cancelable aCancelable) +{ + RefPtr<Event> event = new Event(aOwner, nullptr, nullptr); + + event->InitEvent(aType, + aBubbles == eDoesBubble ? true : false, + aCancelable == eCancelable ? true : false); + + event->SetTrusted(true); + + return event.forget(); +} + +} // namespace indexedDB + +// static +already_AddRefed<IDBVersionChangeEvent> +IDBVersionChangeEvent::CreateInternal(EventTarget* aOwner, + const nsAString& aType, + uint64_t aOldVersion, + Nullable<uint64_t> aNewVersion) +{ + RefPtr<IDBVersionChangeEvent> event = + new IDBVersionChangeEvent(aOwner, aOldVersion); + if (!aNewVersion.IsNull()) { + event->mNewVersion.SetValue(aNewVersion.Value()); + } + + event->InitEvent(aType, false, false); + + event->SetTrusted(true); + + return event.forget(); +} + +already_AddRefed<IDBVersionChangeEvent> +IDBVersionChangeEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const IDBVersionChangeEventInit& aOptions, + ErrorResult& aRv) +{ + nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports()); + + return CreateInternal(target, + aType, + aOptions.mOldVersion, + aOptions.mNewVersion); +} + +NS_IMPL_ADDREF_INHERITED(IDBVersionChangeEvent, Event) +NS_IMPL_RELEASE_INHERITED(IDBVersionChangeEvent, Event) + +NS_INTERFACE_MAP_BEGIN(IDBVersionChangeEvent) + NS_INTERFACE_MAP_ENTRY(IDBVersionChangeEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +JSObject* +IDBVersionChangeEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBVersionChangeEventBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBEvents.h b/dom/indexedDB/IDBEvents.h new file mode 100644 index 000000000..02058fdd8 --- /dev/null +++ b/dom/indexedDB/IDBEvents.h @@ -0,0 +1,134 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbevents_h__ +#define mozilla_dom_idbevents_h__ + +#include "js/RootingAPI.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Nullable.h" + +#define IDBVERSIONCHANGEEVENT_IID \ + {0x3b65d4c3, 0x73ad, 0x492e, {0xb1, 0x2d, 0x15, 0xf9, 0xda, 0xc2, 0x08, 0x4b}} + +class nsAString; +class nsDependentString; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class EventTarget; +class GlobalObject; +struct IDBVersionChangeEventInit; + +namespace indexedDB { + +enum Bubbles { + eDoesNotBubble, + eDoesBubble +}; + +enum Cancelable { + eNotCancelable, + eCancelable +}; + +extern const char16_t* kAbortEventType; +extern const char16_t* kBlockedEventType; +extern const char16_t* kCompleteEventType; +extern const char16_t* kErrorEventType; +extern const char16_t* kSuccessEventType; +extern const char16_t* kUpgradeNeededEventType; +extern const char16_t* kVersionChangeEventType; +extern const char16_t* kCloseEventType; + +already_AddRefed<nsIDOMEvent> +CreateGenericEvent(EventTarget* aOwner, + const nsDependentString& aType, + Bubbles aBubbles, + Cancelable aCancelable); + +} // namespace indexedDB + +class IDBVersionChangeEvent final : public Event +{ + uint64_t mOldVersion; + Nullable<uint64_t> mNewVersion; + +public: + static already_AddRefed<IDBVersionChangeEvent> + Create(EventTarget* aOwner, + const nsDependentString& aName, + uint64_t aOldVersion, + uint64_t aNewVersion) + { + Nullable<uint64_t> newVersion(aNewVersion); + return CreateInternal(aOwner, aName, aOldVersion, newVersion); + } + + static already_AddRefed<IDBVersionChangeEvent> + Create(EventTarget* aOwner, + const nsDependentString& aName, + uint64_t aOldVersion) + { + Nullable<uint64_t> newVersion(0); + newVersion.SetNull(); + return CreateInternal(aOwner, aName, aOldVersion, newVersion); + } + + static already_AddRefed<IDBVersionChangeEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const IDBVersionChangeEventInit& aOptions, + ErrorResult& aRv); + + uint64_t + OldVersion() const + { + return mOldVersion; + } + + Nullable<uint64_t> + GetNewVersion() const + { + return mNewVersion; + } + + NS_DECLARE_STATIC_IID_ACCESSOR(IDBVERSIONCHANGEEVENT_IID) + + NS_DECL_ISUPPORTS_INHERITED + NS_FORWARD_TO_EVENT + + virtual JSObject* + WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBVersionChangeEvent(EventTarget* aOwner, uint64_t aOldVersion) + : Event(aOwner, nullptr, nullptr) + , mOldVersion(aOldVersion) + { + } + + ~IDBVersionChangeEvent() + { } + + static already_AddRefed<IDBVersionChangeEvent> + CreateInternal(EventTarget* aOwner, + const nsAString& aName, + uint64_t aOldVersion, + Nullable<uint64_t> aNewVersion); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(IDBVersionChangeEvent, IDBVERSIONCHANGEEVENT_IID) + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbevents_h__ diff --git a/dom/indexedDB/IDBFactory.cpp b/dom/indexedDB/IDBFactory.cpp new file mode 100644 index 000000000..663828978 --- /dev/null +++ b/dom/indexedDB/IDBFactory.cpp @@ -0,0 +1,934 @@ +/* -*- 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 "IDBFactory.h" + +#include "BackgroundChildImpl.h" +#include "IDBRequest.h" +#include "IndexedDatabaseManager.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IDBFactoryBinding.h" +#include "mozilla/dom/TabChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackground.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozIThirdPartyUtil.h" +#include "nsAboutProtocolUtils.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsIAboutModule.h" +#include "nsIIPCBackgroundChildCreateCallback.h" +#include "nsILoadContext.h" +#include "nsIPrincipal.h" +#include "nsIURI.h" +#include "nsIUUIDGenerator.h" +#include "nsIWebNavigation.h" +#include "nsSandboxFlags.h" +#include "nsServiceManagerUtils.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +#ifdef DEBUG +#include "nsContentUtils.h" // For assertions. +#endif + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; + +namespace { + +const char kPrefIndexedDBEnabled[] = "dom.indexedDB.enabled"; + +} // namespace + +class IDBFactory::BackgroundCreateCallback final + : public nsIIPCBackgroundChildCreateCallback +{ + RefPtr<IDBFactory> mFactory; + LoggingInfo mLoggingInfo; + +public: + explicit + BackgroundCreateCallback(IDBFactory* aFactory, + const LoggingInfo& aLoggingInfo) + : mFactory(aFactory) + , mLoggingInfo(aLoggingInfo) + { + MOZ_ASSERT(aFactory); + } + + NS_DECL_ISUPPORTS + +private: + ~BackgroundCreateCallback() + { } + + NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK +}; + +struct IDBFactory::PendingRequestInfo +{ + RefPtr<IDBOpenDBRequest> mRequest; + FactoryRequestParams mParams; + + PendingRequestInfo(IDBOpenDBRequest* aRequest, + const FactoryRequestParams& aParams) + : mRequest(aRequest), mParams(aParams) + { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aParams.type() != FactoryRequestParams::T__None); + } +}; + +IDBFactory::IDBFactory() + : mOwningObject(nullptr) + , mBackgroundActor(nullptr) + , mInnerWindowID(0) + , mBackgroundActorFailed(false) + , mPrivateBrowsingMode(false) +{ +#ifdef DEBUG + mOwningThread = PR_GetCurrentThread(); +#endif + AssertIsOnOwningThread(); +} + +IDBFactory::~IDBFactory() +{ + MOZ_ASSERT_IF(mBackgroundActorFailed, !mBackgroundActor); + + mOwningObject = nullptr; + mozilla::DropJSObjects(this); + + if (mBackgroundActor) { + mBackgroundActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } +} + +// static +nsresult +IDBFactory::CreateForWindow(nsPIDOMWindowInner* aWindow, + IDBFactory** aFactory) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aFactory); + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = AllowedForWindowInternal(aWindow, getter_AddRefs(principal)); + + if (!(NS_SUCCEEDED(rv) && nsContentUtils::IsSystemPrincipal(principal)) && + NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) { + *aFactory = nullptr; + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + if (rv == NS_ERROR_DOM_NOT_SUPPORTED_ERR) { + NS_WARNING("IndexedDB is not permitted in a third-party window."); + *aFactory = nullptr; + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + if (rv == NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR) { + IDB_REPORT_INTERNAL_ERR(); + } + return rv; + } + + MOZ_ASSERT(principal); + + nsAutoPtr<PrincipalInfo> principalInfo(new PrincipalInfo()); + rv = PrincipalToPrincipalInfo(principal, principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo || + principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); + + nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(aWindow); + nsCOMPtr<nsILoadContext> loadContext = do_QueryInterface(webNav); + + RefPtr<IDBFactory> factory = new IDBFactory(); + factory->mPrincipalInfo = Move(principalInfo); + factory->mWindow = aWindow; + factory->mTabChild = TabChild::GetFrom(aWindow); + factory->mInnerWindowID = aWindow->WindowID(); + factory->mPrivateBrowsingMode = + loadContext && loadContext->UsePrivateBrowsing(); + + factory.forget(aFactory); + return NS_OK; +} + +// static +nsresult +IDBFactory::CreateForMainThreadJS(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + IDBFactory** aFactory) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoPtr<PrincipalInfo> principalInfo(new PrincipalInfo()); + nsIPrincipal* principal = nsContentUtils::ObjectPrincipal(aOwningObject); + MOZ_ASSERT(principal); + bool isSystem; + if (!AllowedForPrincipal(principal, &isSystem)) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsresult rv = PrincipalToPrincipalInfo(principal, principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = CreateForMainThreadJSInternal(aCx, aOwningObject, principalInfo, aFactory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!principalInfo); + + return NS_OK; +} + +// static +nsresult +IDBFactory::CreateForWorker(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + const PrincipalInfo& aPrincipalInfo, + uint64_t aInnerWindowID, + IDBFactory** aFactory) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPrincipalInfo.type() != PrincipalInfo::T__None); + + nsAutoPtr<PrincipalInfo> principalInfo(new PrincipalInfo(aPrincipalInfo)); + + nsresult rv = + CreateForJSInternal(aCx, + aOwningObject, + principalInfo, + aInnerWindowID, + aFactory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!principalInfo); + + return NS_OK; +} + +// static +nsresult +IDBFactory::CreateForMainThreadJSInternal( + JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + nsAutoPtr<PrincipalInfo>& aPrincipalInfo, + IDBFactory** aFactory) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipalInfo); + + if (aPrincipalInfo->type() != PrincipalInfo::TSystemPrincipalInfo && + NS_WARN_IF(!Preferences::GetBool(kPrefIndexedDBEnabled, false))) { + *aFactory = nullptr; + return NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + } + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetOrCreate(); + if (NS_WARN_IF(!mgr)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsresult rv = + CreateForJSInternal(aCx, + aOwningObject, + aPrincipalInfo, + /* aInnerWindowID */ 0, + aFactory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +nsresult +IDBFactory::CreateForJSInternal(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + nsAutoPtr<PrincipalInfo>& aPrincipalInfo, + uint64_t aInnerWindowID, + IDBFactory** aFactory) +{ + MOZ_ASSERT(aCx); + MOZ_ASSERT(aOwningObject); + MOZ_ASSERT(aPrincipalInfo); + MOZ_ASSERT(aPrincipalInfo->type() != PrincipalInfo::T__None); + MOZ_ASSERT(aFactory); + MOZ_ASSERT(JS_GetGlobalForObject(aCx, aOwningObject) == aOwningObject, + "Not a global object!"); + + if (aPrincipalInfo->type() != PrincipalInfo::TContentPrincipalInfo && + aPrincipalInfo->type() != PrincipalInfo::TSystemPrincipalInfo) { + NS_WARNING("IndexedDB not allowed for this principal!"); + aPrincipalInfo = nullptr; + *aFactory = nullptr; + return NS_OK; + } + + RefPtr<IDBFactory> factory = new IDBFactory(); + factory->mPrincipalInfo = aPrincipalInfo.forget(); + factory->mOwningObject = aOwningObject; + mozilla::HoldJSObjects(factory.get()); + factory->mInnerWindowID = aInnerWindowID; + + factory.forget(aFactory); + return NS_OK; +} + +// static +bool +IDBFactory::AllowedForWindow(nsPIDOMWindowInner* aWindow) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = AllowedForWindowInternal(aWindow, getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +// static +nsresult +IDBFactory::AllowedForWindowInternal(nsPIDOMWindowInner* aWindow, + nsIPrincipal** aPrincipal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + if (NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate())) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsContentUtils::StorageAccess access = + nsContentUtils::StorageAllowedForWindow(aWindow); + + // the factory callsite records whether the browser is in private browsing. + // and thus we don't have to respect that setting here. IndexedDB has no + // concept of session-local storage, and thus ignores it. + if (access == nsContentUtils::StorageAccess::eDeny) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + MOZ_ASSERT(sop); + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + + } + + if (nsContentUtils::IsSystemPrincipal(principal)) { + principal.forget(aPrincipal); + return NS_OK; + } + + // About URIs shouldn't be able to access IndexedDB unless they have the + // nsIAboutModule::ENABLE_INDEXED_DB flag set on them. + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(principal->GetURI(getter_AddRefs(uri))); + MOZ_ASSERT(uri); + + bool isAbout = false; + MOZ_ALWAYS_SUCCEEDS(uri->SchemeIs("about", &isAbout)); + + if (isAbout) { + nsCOMPtr<nsIAboutModule> module; + if (NS_SUCCEEDED(NS_GetAboutModule(uri, getter_AddRefs(module)))) { + uint32_t flags; + if (NS_SUCCEEDED(module->GetURIFlags(uri, &flags))) { + if (!(flags & nsIAboutModule::ENABLE_INDEXED_DB)) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } else { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } else { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } + + principal.forget(aPrincipal); + return NS_OK; +} + +// static +bool +IDBFactory::AllowedForPrincipal(nsIPrincipal* aPrincipal, + bool* aIsSystemPrincipal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate())) { + return false; + } + + if (nsContentUtils::IsSystemPrincipal(aPrincipal)) { + if (aIsSystemPrincipal) { + *aIsSystemPrincipal = true; + } + return true; + } else if (aIsSystemPrincipal) { + *aIsSystemPrincipal = false; + } + + if (aPrincipal->GetIsNullPrincipal()) { + return false; + } + + return true; +} + +#ifdef DEBUG + +void +IDBFactory::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mOwningThread); + MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread); +} + +PRThread* +IDBFactory::OwningThread() const +{ + MOZ_ASSERT(mOwningThread); + return mOwningThread; +} + +#endif // DEBUG + +bool +IDBFactory::IsChrome() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrincipalInfo); + + return mPrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo; +} + +void +IDBFactory::IncrementParentLoggingRequestSerialNumber() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mBackgroundActor); + + mBackgroundActor->SendIncrementLoggingRequestSerialNumber(); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::Open(JSContext* aCx, + const nsAString& aName, + uint64_t aVersion, + ErrorResult& aRv) +{ + return OpenInternal(aCx, + /* aPrincipal */ nullptr, + aName, + Optional<uint64_t>(aVersion), + Optional<StorageType>(), + /* aDeleting */ false, + aRv); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::Open(JSContext* aCx, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv) +{ + return OpenInternal(aCx, + /* aPrincipal */ nullptr, + aName, + aOptions.mVersion, + aOptions.mStorage, + /* aDeleting */ false, + aRv); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::DeleteDatabase(JSContext* aCx, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv) +{ + return OpenInternal(aCx, + /* aPrincipal */ nullptr, + aName, + Optional<uint64_t>(), + aOptions.mStorage, + /* aDeleting */ true, + aRv); +} + +int16_t +IDBFactory::Cmp(JSContext* aCx, JS::Handle<JS::Value> aFirst, + JS::Handle<JS::Value> aSecond, ErrorResult& aRv) +{ + Key first, second; + nsresult rv = first.SetFromJSVal(aCx, aFirst); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return 0; + } + + rv = second.SetFromJSVal(aCx, aSecond); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return 0; + } + + if (first.IsUnset() || second.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return 0; + } + + return Key::CompareKeys(first, second); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::OpenForPrincipal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + uint64_t aVersion, + ErrorResult& aRv) +{ + MOZ_ASSERT(aPrincipal); + if (!NS_IsMainThread()) { + MOZ_CRASH("Figure out security checks for workers!"); + } + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + return OpenInternal(aCx, + aPrincipal, + aName, + Optional<uint64_t>(aVersion), + Optional<StorageType>(), + /* aDeleting */ false, + aRv); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::OpenForPrincipal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv) +{ + MOZ_ASSERT(aPrincipal); + if (!NS_IsMainThread()) { + MOZ_CRASH("Figure out security checks for workers!"); + } + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + return OpenInternal(aCx, + aPrincipal, + aName, + aOptions.mVersion, + aOptions.mStorage, + /* aDeleting */ false, + aRv); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::DeleteForPrincipal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv) +{ + MOZ_ASSERT(aPrincipal); + if (!NS_IsMainThread()) { + MOZ_CRASH("Figure out security checks for workers!"); + } + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + return OpenInternal(aCx, + aPrincipal, + aName, + Optional<uint64_t>(), + aOptions.mStorage, + /* aDeleting */ true, + aRv); +} + +already_AddRefed<IDBOpenDBRequest> +IDBFactory::OpenInternal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + const Optional<uint64_t>& aVersion, + const Optional<StorageType>& aStorageType, + bool aDeleting, + ErrorResult& aRv) +{ + MOZ_ASSERT(mWindow || mOwningObject); + MOZ_ASSERT_IF(!mWindow, !mPrivateBrowsingMode); + + CommonFactoryRequestParams commonParams; + + PrincipalInfo& principalInfo = commonParams.principalInfo(); + + if (aPrincipal) { + if (!NS_IsMainThread()) { + MOZ_CRASH("Figure out security checks for workers!"); + } + MOZ_ASSERT(nsContentUtils::IsCallerChrome()); + + if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, + &principalInfo)))) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + if (principalInfo.type() != PrincipalInfo::TContentPrincipalInfo && + principalInfo.type() != PrincipalInfo::TSystemPrincipalInfo) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + } else { + principalInfo = *mPrincipalInfo; + } + + uint64_t version = 0; + if (!aDeleting && aVersion.WasPassed()) { + if (aVersion.Value() < 1) { + aRv.ThrowTypeError<MSG_INVALID_VERSION>(); + return nullptr; + } + version = aVersion.Value(); + } + + // Nothing can be done here if we have previously failed to create a + // background actor. + if (mBackgroundActorFailed) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + PersistenceType persistenceType; + + if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + // Chrome privilege always gets persistent storage. + persistenceType = PERSISTENCE_TYPE_PERSISTENT; + } else { + persistenceType = PersistenceTypeFromStorage(aStorageType); + } + + DatabaseMetadata& metadata = commonParams.metadata(); + metadata.name() = aName; + metadata.persistenceType() = persistenceType; + + FactoryRequestParams params; + if (aDeleting) { + metadata.version() = 0; + params = DeleteDatabaseRequestParams(commonParams); + } else { + metadata.version() = version; + params = OpenDatabaseRequestParams(commonParams); + } + + if (!mBackgroundActor && mPendingRequests.IsEmpty()) { + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + + nsAutoPtr<ThreadLocal> newIDBThreadLocal; + ThreadLocal* idbThreadLocal; + + if (threadLocal && threadLocal->mIndexedDBThreadLocal) { + idbThreadLocal = threadLocal->mIndexedDBThreadLocal; + } else { + nsCOMPtr<nsIUUIDGenerator> uuidGen = + do_GetService("@mozilla.org/uuid-generator;1"); + MOZ_ASSERT(uuidGen); + + nsID id; + MOZ_ALWAYS_SUCCEEDS(uuidGen->GenerateUUIDInPlace(&id)); + + newIDBThreadLocal = idbThreadLocal = new ThreadLocal(id); + } + + if (PBackgroundChild* actor = BackgroundChild::GetForCurrentThread()) { + BackgroundActorCreated(actor, idbThreadLocal->GetLoggingInfo()); + } else { + // We need to start the sequence to create a background actor for this + // thread. + RefPtr<BackgroundCreateCallback> cb = + new BackgroundCreateCallback(this, idbThreadLocal->GetLoggingInfo()); + if (NS_WARN_IF(!BackgroundChild::GetOrCreateForCurrentThread(cb))) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + } + + if (newIDBThreadLocal) { + if (!threadLocal) { + threadLocal = BackgroundChildImpl::GetThreadLocalForCurrentThread(); + } + MOZ_ASSERT(threadLocal); + MOZ_ASSERT(!threadLocal->mIndexedDBThreadLocal); + + threadLocal->mIndexedDBThreadLocal = newIDBThreadLocal.forget(); + } + } + + RefPtr<IDBOpenDBRequest> request; + + if (mWindow) { + JS::Rooted<JSObject*> scriptOwner(aCx, + nsGlobalWindow::Cast(mWindow.get())->FastGetGlobalJSObject()); + MOZ_ASSERT(scriptOwner); + + request = IDBOpenDBRequest::CreateForWindow(aCx, this, mWindow, scriptOwner); + } else { + JS::Rooted<JSObject*> scriptOwner(aCx, mOwningObject); + + request = IDBOpenDBRequest::CreateForJS(aCx, this, scriptOwner); + if (!request) { + MOZ_ASSERT(!NS_IsMainThread()); + aRv.ThrowUncatchableException(); + return nullptr; + } + } + + MOZ_ASSERT(request); + + if (aDeleting) { + IDB_LOG_MARK("IndexedDB %s: Child Request[%llu]: " + "indexedDB.deleteDatabase(\"%s\")", + "IndexedDB %s: C R[%llu]: IDBFactory.deleteDatabase()", + IDB_LOG_ID_STRING(), + request->LoggingSerialNumber(), + NS_ConvertUTF16toUTF8(aName).get()); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Request[%llu]: " + "indexedDB.open(\"%s\", %s)", + "IndexedDB %s: C R[%llu]: IDBFactory.open()", + IDB_LOG_ID_STRING(), + request->LoggingSerialNumber(), + NS_ConvertUTF16toUTF8(aName).get(), + IDB_LOG_STRINGIFY(aVersion)); + } + + // If we already have a background actor then we can start this request now. + if (mBackgroundActor) { + nsresult rv = InitiateRequest(request, params); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + } else { + mPendingRequests.AppendElement(new PendingRequestInfo(request, params)); + } + + return request.forget(); +} + +nsresult +IDBFactory::BackgroundActorCreated(PBackgroundChild* aBackgroundActor, + const LoggingInfo& aLoggingInfo) +{ + MOZ_ASSERT(aBackgroundActor); + MOZ_ASSERT(!mBackgroundActor); + MOZ_ASSERT(!mBackgroundActorFailed); + + { + BackgroundFactoryChild* actor = new BackgroundFactoryChild(this); + + mBackgroundActor = + static_cast<BackgroundFactoryChild*>( + aBackgroundActor->SendPBackgroundIDBFactoryConstructor(actor, + aLoggingInfo)); + } + + if (NS_WARN_IF(!mBackgroundActor)) { + BackgroundActorFailed(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsresult rv = NS_OK; + + for (uint32_t index = 0, count = mPendingRequests.Length(); + index < count; + index++) { + nsAutoPtr<PendingRequestInfo> info(mPendingRequests[index].forget()); + + nsresult rv2 = InitiateRequest(info->mRequest, info->mParams); + + // Warn for every failure, but just return the first failure if there are + // multiple failures. + if (NS_WARN_IF(NS_FAILED(rv2)) && NS_SUCCEEDED(rv)) { + rv = rv2; + } + } + + mPendingRequests.Clear(); + + return rv; +} + +void +IDBFactory::BackgroundActorFailed() +{ + MOZ_ASSERT(!mPendingRequests.IsEmpty()); + MOZ_ASSERT(!mBackgroundActor); + MOZ_ASSERT(!mBackgroundActorFailed); + + mBackgroundActorFailed = true; + + for (uint32_t index = 0, count = mPendingRequests.Length(); + index < count; + index++) { + nsAutoPtr<PendingRequestInfo> info(mPendingRequests[index].forget()); + info->mRequest-> + DispatchNonTransactionError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + mPendingRequests.Clear(); +} + +nsresult +IDBFactory::InitiateRequest(IDBOpenDBRequest* aRequest, + const FactoryRequestParams& aParams) +{ + MOZ_ASSERT(aRequest); + MOZ_ASSERT(mBackgroundActor); + MOZ_ASSERT(!mBackgroundActorFailed); + + bool deleting; + uint64_t requestedVersion; + + switch (aParams.type()) { + case FactoryRequestParams::TDeleteDatabaseRequestParams: { + const DatabaseMetadata& metadata = + aParams.get_DeleteDatabaseRequestParams().commonParams().metadata(); + deleting = true; + requestedVersion = metadata.version(); + break; + } + + case FactoryRequestParams::TOpenDatabaseRequestParams: { + const DatabaseMetadata& metadata = + aParams.get_OpenDatabaseRequestParams().commonParams().metadata(); + deleting = false; + requestedVersion = metadata.version(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + auto actor = + new BackgroundFactoryRequestChild(this, + aRequest, + deleting, + requestedVersion); + + if (!mBackgroundActor->SendPBackgroundIDBFactoryRequestConstructor(actor, + aParams)) { + aRequest->DispatchNonTransactionError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBFactory) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBFactory) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBFactory) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBFactory) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBFactory) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBFactory) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + tmp->mOwningObject = nullptr; + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBFactory) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOwningObject) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +JSObject* +IDBFactory::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBFactoryBinding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ISUPPORTS(IDBFactory::BackgroundCreateCallback, + nsIIPCBackgroundChildCreateCallback) + +void +IDBFactory::BackgroundCreateCallback::ActorCreated(PBackgroundChild* aActor) +{ + MOZ_ASSERT(aActor); + MOZ_ASSERT(mFactory); + + RefPtr<IDBFactory> factory; + mFactory.swap(factory); + + factory->BackgroundActorCreated(aActor, mLoggingInfo); +} + +void +IDBFactory::BackgroundCreateCallback::ActorFailed() +{ + MOZ_ASSERT(mFactory); + + RefPtr<IDBFactory> factory; + mFactory.swap(factory); + + factory->BackgroundActorFailed(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBFactory.h b/dom/indexedDB/IDBFactory.h new file mode 100644 index 000000000..15c53530d --- /dev/null +++ b/dom/indexedDB/IDBFactory.h @@ -0,0 +1,258 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbfactory_h__ +#define mozilla_dom_idbfactory_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/StorageTypeBinding.h" +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +class nsIPrincipal; +class nsPIDOMWindowInner; +struct PRThread; + +namespace mozilla { + +class ErrorResult; + +namespace ipc { + +class PBackgroundChild; +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +struct IDBOpenDBOptions; +class IDBOpenDBRequest; +template <typename> class Optional; +class TabChild; + +namespace indexedDB { +class BackgroundFactoryChild; +class FactoryRequestParams; +class LoggingInfo; +} + +class IDBFactory final + : public nsISupports + , public nsWrapperCache +{ + typedef mozilla::dom::StorageType StorageType; + typedef mozilla::ipc::PBackgroundChild PBackgroundChild; + typedef mozilla::ipc::PrincipalInfo PrincipalInfo; + + class BackgroundCreateCallback; + struct PendingRequestInfo; + + nsAutoPtr<PrincipalInfo> mPrincipalInfo; + + // If this factory lives on a window then mWindow must be non-null. Otherwise + // mOwningObject must be non-null. + nsCOMPtr<nsPIDOMWindowInner> mWindow; + JS::Heap<JSObject*> mOwningObject; + + // This will only be set if the factory belongs to a window in a child + // process. + RefPtr<TabChild> mTabChild; + + nsTArray<nsAutoPtr<PendingRequestInfo>> mPendingRequests; + + indexedDB::BackgroundFactoryChild* mBackgroundActor; + +#ifdef DEBUG + PRThread* mOwningThread; +#endif + + uint64_t mInnerWindowID; + + bool mBackgroundActorFailed; + bool mPrivateBrowsingMode; + +public: + static nsresult + CreateForWindow(nsPIDOMWindowInner* aWindow, + IDBFactory** aFactory); + + static nsresult + CreateForMainThreadJS(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + IDBFactory** aFactory); + + static nsresult + CreateForWorker(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + const PrincipalInfo& aPrincipalInfo, + uint64_t aInnerWindowID, + IDBFactory** aFactory); + + static bool + AllowedForWindow(nsPIDOMWindowInner* aWindow); + + static bool + AllowedForPrincipal(nsIPrincipal* aPrincipal, + bool* aIsSystemPrincipal = nullptr); + +#ifdef DEBUG + void + AssertIsOnOwningThread() const; + + PRThread* + OwningThread() const; +#else + void + AssertIsOnOwningThread() const + { } +#endif + + void + ClearBackgroundActor() + { + AssertIsOnOwningThread(); + + mBackgroundActor = nullptr; + } + + void + IncrementParentLoggingRequestSerialNumber(); + + nsPIDOMWindowInner* + GetParentObject() const + { + return mWindow; + } + + TabChild* + GetTabChild() const + { + return mTabChild; + } + + PrincipalInfo* + GetPrincipalInfo() const + { + AssertIsOnOwningThread(); + + return mPrincipalInfo; + } + + uint64_t + InnerWindowID() const + { + AssertIsOnOwningThread(); + + return mInnerWindowID; + } + + bool + IsChrome() const; + + already_AddRefed<IDBOpenDBRequest> + Open(JSContext* aCx, + const nsAString& aName, + uint64_t aVersion, + ErrorResult& aRv); + + already_AddRefed<IDBOpenDBRequest> + Open(JSContext* aCx, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed<IDBOpenDBRequest> + DeleteDatabase(JSContext* aCx, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv); + + int16_t + Cmp(JSContext* aCx, + JS::Handle<JS::Value> aFirst, + JS::Handle<JS::Value> aSecond, + ErrorResult& aRv); + + already_AddRefed<IDBOpenDBRequest> + OpenForPrincipal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + uint64_t aVersion, + ErrorResult& aRv); + + already_AddRefed<IDBOpenDBRequest> + OpenForPrincipal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed<IDBOpenDBRequest> + DeleteForPrincipal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBFactory) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBFactory(); + ~IDBFactory(); + + static nsresult + CreateForMainThreadJSInternal(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + nsAutoPtr<PrincipalInfo>& aPrincipalInfo, + IDBFactory** aFactory); + + static nsresult + CreateForJSInternal(JSContext* aCx, + JS::Handle<JSObject*> aOwningObject, + nsAutoPtr<PrincipalInfo>& aPrincipalInfo, + uint64_t aInnerWindowID, + IDBFactory** aFactory); + + static nsresult + AllowedForWindowInternal(nsPIDOMWindowInner* aWindow, + nsIPrincipal** aPrincipal); + + already_AddRefed<IDBOpenDBRequest> + OpenInternal(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsAString& aName, + const Optional<uint64_t>& aVersion, + const Optional<StorageType>& aStorageType, + bool aDeleting, + ErrorResult& aRv); + + nsresult + BackgroundActorCreated(PBackgroundChild* aBackgroundActor, + const indexedDB::LoggingInfo& aLoggingInfo); + + void + BackgroundActorFailed(); + + nsresult + InitiateRequest(IDBOpenDBRequest* aRequest, + const indexedDB::FactoryRequestParams& aParams); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbfactory_h__ diff --git a/dom/indexedDB/IDBFileHandle.cpp b/dom/indexedDB/IDBFileHandle.cpp new file mode 100644 index 000000000..8b88e1722 --- /dev/null +++ b/dom/indexedDB/IDBFileHandle.cpp @@ -0,0 +1,199 @@ +/* -*- 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 "IDBFileHandle.h" + +#include "IDBEvents.h" +#include "IDBMutableFile.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/IDBFileHandleBinding.h" +#include "mozilla/dom/filehandle/ActorsChild.h" +#include "mozilla/EventDispatcher.h" +#include "nsServiceManagerUtils.h" +#include "nsWidgetsCID.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::indexedDB; + +IDBFileHandle::IDBFileHandle(FileMode aMode, + IDBMutableFile* aMutableFile) + : FileHandleBase(DEBUGONLY(aMutableFile->OwningThread(),) + aMode) + , mMutableFile(aMutableFile) +{ + AssertIsOnOwningThread(); +} + +IDBFileHandle::~IDBFileHandle() +{ + AssertIsOnOwningThread(); + + mMutableFile->UnregisterFileHandle(this); +} + +// static +already_AddRefed<IDBFileHandle> +IDBFileHandle::Create(IDBMutableFile* aMutableFile, + FileMode aMode) +{ + MOZ_ASSERT(aMutableFile); + aMutableFile->AssertIsOnOwningThread(); + MOZ_ASSERT(aMode == FileMode::Readonly || aMode == FileMode::Readwrite); + + RefPtr<IDBFileHandle> fileHandle = + new IDBFileHandle(aMode, aMutableFile); + + fileHandle->BindToOwner(aMutableFile); + + // XXX Fix! + MOZ_ASSERT(NS_IsMainThread(), "This won't work on non-main threads!"); + + nsCOMPtr<nsIRunnable> runnable = do_QueryObject(fileHandle); + nsContentUtils::RunInMetastableState(runnable.forget()); + + fileHandle->SetCreating(); + + aMutableFile->RegisterFileHandle(fileHandle); + + return fileHandle.forget(); +} + +already_AddRefed<IDBFileRequest> +IDBFileHandle::GetMetadata(const IDBFileMetadataParameters& aParameters, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + // Common state checking + if (!CheckState(aRv)) { + return nullptr; + } + + // Argument checking for get metadata. + if (!aParameters.mSize && !aParameters.mLastModified) { + aRv.ThrowTypeError<MSG_METADATA_NOT_CONFIGURED>(); + return nullptr; + } + + // Do nothing if the window is closed + if (!CheckWindow()) { + return nullptr; + } + + FileRequestGetMetadataParams params; + params.size() = aParameters.mSize; + params.lastModified() = aParameters.mLastModified; + + RefPtr<FileRequestBase> fileRequest = GenerateFileRequest(); + + StartRequest(fileRequest, params); + + return fileRequest.forget().downcast<IDBFileRequest>(); +} + +NS_IMPL_ADDREF_INHERITED(IDBFileHandle, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(IDBFileHandle, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBFileHandle) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBFileHandle) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBFileHandle, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMutableFile) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBFileHandle, + DOMEventTargetHelper) + // Don't unlink mMutableFile! +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMETHODIMP +IDBFileHandle::Run() +{ + AssertIsOnOwningThread(); + + OnReturnToEventLoop(); + + return NS_OK; +} + +nsresult +IDBFileHandle::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + AssertIsOnOwningThread(); + + aVisitor.mCanHandle = true; + aVisitor.mParentTarget = mMutableFile; + return NS_OK; +} + +// virtual +JSObject* +IDBFileHandle::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnOwningThread(); + + return IDBFileHandleBinding::Wrap(aCx, this, aGivenProto); +} + +mozilla::dom::MutableFileBase* +IDBFileHandle::MutableFile() const +{ + AssertIsOnOwningThread(); + + return mMutableFile; +} + +void +IDBFileHandle::HandleCompleteOrAbort(bool aAborted) +{ + AssertIsOnOwningThread(); + + FileHandleBase::HandleCompleteOrAbort(aAborted); + + nsCOMPtr<nsIDOMEvent> event; + if (aAborted) { + event = CreateGenericEvent(this, nsDependentString(kAbortEventType), + eDoesBubble, eNotCancelable); + } else { + event = CreateGenericEvent(this, nsDependentString(kCompleteEventType), + eDoesNotBubble, eNotCancelable); + } + if (NS_WARN_IF(!event)) { + return; + } + + bool dummy; + if (NS_FAILED(DispatchEvent(event, &dummy))) { + NS_WARNING("DispatchEvent failed!"); + } +} + +bool +IDBFileHandle::CheckWindow() +{ + AssertIsOnOwningThread(); + + return GetOwner(); +} + +already_AddRefed<mozilla::dom::FileRequestBase> +IDBFileHandle::GenerateFileRequest() +{ + AssertIsOnOwningThread(); + + return IDBFileRequest::Create(GetOwner(), this, + /* aWrapAsDOMRequest */ false); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBFileHandle.h b/dom/indexedDB/IDBFileHandle.h new file mode 100644 index 000000000..574524090 --- /dev/null +++ b/dom/indexedDB/IDBFileHandle.h @@ -0,0 +1,149 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbfilehandle_h__ +#define mozilla_dom_idbfilehandle_h__ + +#include "IDBFileRequest.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/FileHandleBase.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIRunnable.h" +#include "nsWeakReference.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +struct IDBFileMetadataParameters; +class IDBFileRequest; +class IDBMutableFile; + +class IDBFileHandle final + : public DOMEventTargetHelper + , public nsIRunnable + , public FileHandleBase + , public nsSupportsWeakReference +{ + RefPtr<IDBMutableFile> mMutableFile; + +public: + static already_AddRefed<IDBFileHandle> + Create(IDBMutableFile* aMutableFile, + FileMode aMode); + + // WebIDL + nsPIDOMWindowInner* + GetParentObject() const + { + AssertIsOnOwningThread(); + return GetOwner(); + } + + IDBMutableFile* + GetMutableFile() const + { + AssertIsOnOwningThread(); + return mMutableFile; + } + + IDBMutableFile* + GetFileHandle() const + { + AssertIsOnOwningThread(); + return GetMutableFile(); + } + + already_AddRefed<IDBFileRequest> + GetMetadata(const IDBFileMetadataParameters& aParameters, ErrorResult& aRv); + + already_AddRefed<IDBFileRequest> + ReadAsArrayBuffer(uint64_t aSize, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + return Read(aSize, false, NullString(), aRv).downcast<IDBFileRequest>(); + } + + already_AddRefed<IDBFileRequest> + ReadAsText(uint64_t aSize, const nsAString& aEncoding, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + return Read(aSize, true, aEncoding, aRv).downcast<IDBFileRequest>(); + } + + already_AddRefed<IDBFileRequest> + Write(const StringOrArrayBufferOrArrayBufferViewOrBlob& aValue, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + return WriteOrAppend(aValue, false, aRv).downcast<IDBFileRequest>(); + } + + already_AddRefed<IDBFileRequest> + Append(const StringOrArrayBufferOrArrayBufferViewOrBlob& aValue, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + return WriteOrAppend(aValue, true, aRv).downcast<IDBFileRequest>(); + } + + already_AddRefed<IDBFileRequest> + Truncate(const Optional<uint64_t>& aSize, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + return FileHandleBase::Truncate(aSize, aRv).downcast<IDBFileRequest>(); + } + + already_AddRefed<IDBFileRequest> + Flush(ErrorResult& aRv) + { + AssertIsOnOwningThread(); + return FileHandleBase::Flush(aRv).downcast<IDBFileRequest>(); + } + + IMPL_EVENT_HANDLER(complete) + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(error) + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBFileHandle, DOMEventTargetHelper) + + // nsIDOMEventTarget + virtual nsresult + PreHandleEvent(EventChainPreVisitor& aVisitor) override; + + // WrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + // FileHandleBase + virtual MutableFileBase* + MutableFile() const override; + + virtual void + HandleCompleteOrAbort(bool aAborted) override; + +private: + IDBFileHandle(FileMode aMode, + IDBMutableFile* aMutableFile); + ~IDBFileHandle(); + + // FileHandleBase + virtual bool + CheckWindow() override; + + virtual already_AddRefed<FileRequestBase> + GenerateFileRequest() override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbfilehandle_h__ diff --git a/dom/indexedDB/IDBFileRequest.cpp b/dom/indexedDB/IDBFileRequest.cpp new file mode 100644 index 000000000..066b2b24a --- /dev/null +++ b/dom/indexedDB/IDBFileRequest.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "IDBFileRequest.h" + +#include "IDBFileHandle.h" +#include "js/RootingAPI.h" +#include "jsapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/IDBFileRequestBinding.h" +#include "mozilla/dom/ProgressEvent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/EventDispatcher.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsLiteralString.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::indexedDB; + +IDBFileRequest::IDBFileRequest(nsPIDOMWindowInner* aWindow, + IDBFileHandle* aFileHandle, + bool aWrapAsDOMRequest) + : DOMRequest(aWindow) + , FileRequestBase(DEBUGONLY(aFileHandle->OwningThread())) + , mFileHandle(aFileHandle) + , mWrapAsDOMRequest(aWrapAsDOMRequest) +{ + AssertIsOnOwningThread(); +} + +IDBFileRequest::~IDBFileRequest() +{ + AssertIsOnOwningThread(); +} + +// static +already_AddRefed<IDBFileRequest> +IDBFileRequest::Create(nsPIDOMWindowInner* aOwner, IDBFileHandle* aFileHandle, + bool aWrapAsDOMRequest) +{ + MOZ_ASSERT(aFileHandle); + aFileHandle->AssertIsOnOwningThread(); + + RefPtr<IDBFileRequest> request = + new IDBFileRequest(aOwner, aFileHandle, aWrapAsDOMRequest); + + return request.forget(); +} + +NS_IMPL_ADDREF_INHERITED(IDBFileRequest, DOMRequest) +NS_IMPL_RELEASE_INHERITED(IDBFileRequest, DOMRequest) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBFileRequest) +NS_INTERFACE_MAP_END_INHERITING(DOMRequest) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(IDBFileRequest, DOMRequest, + mFileHandle) + +nsresult +IDBFileRequest::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + AssertIsOnOwningThread(); + + aVisitor.mCanHandle = true; + aVisitor.mParentTarget = mFileHandle; + return NS_OK; +} + +// virtual +JSObject* +IDBFileRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnOwningThread(); + + if (mWrapAsDOMRequest) { + return DOMRequest::WrapObject(aCx, aGivenProto); + } + return IDBFileRequestBinding::Wrap(aCx, this, aGivenProto); +} + +mozilla::dom::FileHandleBase* +IDBFileRequest::FileHandle() const +{ + AssertIsOnOwningThread(); + + return mFileHandle; +} + +void +IDBFileRequest::OnProgress(uint64_t aProgress, uint64_t aProgressMax) +{ + AssertIsOnOwningThread(); + + FireProgressEvent(aProgress, aProgressMax); +} + +void +IDBFileRequest::SetResultCallback(ResultCallback* aCallback) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + + AutoJSAPI autoJS; + if (NS_WARN_IF(!autoJS.Init(GetOwner()))) { + FireError(NS_ERROR_DOM_FILEHANDLE_UNKNOWN_ERR); + return; + } + + JSContext* cx = autoJS.cx(); + + JS::Rooted<JS::Value> result(cx); + nsresult rv = aCallback->GetResult(cx, &result); + if (NS_WARN_IF(NS_FAILED(rv))) { + FireError(rv); + } else { + FireSuccess(result); + } +} + +void +IDBFileRequest::SetError(nsresult aError) +{ + AssertIsOnOwningThread(); + + FireError(aError); +} + +void +IDBFileRequest::FireProgressEvent(uint64_t aLoaded, uint64_t aTotal) +{ + AssertIsOnOwningThread(); + + if (NS_FAILED(CheckInnerWindowCorrectness())) { + return; + } + + ProgressEventInit init; + init.mBubbles = false; + init.mCancelable = false; + init.mLengthComputable = false; + init.mLoaded = aLoaded; + init.mTotal = aTotal; + + RefPtr<ProgressEvent> event = + ProgressEvent::Constructor(this, NS_LITERAL_STRING("progress"), init); + DispatchTrustedEvent(event); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBFileRequest.h b/dom/indexedDB/IDBFileRequest.h new file mode 100644 index 000000000..4f4252dc9 --- /dev/null +++ b/dom/indexedDB/IDBFileRequest.h @@ -0,0 +1,94 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbfilerequest_h__ +#define mozilla_dom_idbfilerequest_h__ + +#include "DOMRequest.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/FileRequestBase.h" +#include "nsCycleCollectionParticipant.h" + +template <class> struct already_AddRefed; +class nsPIDOMWindowInner; + +namespace mozilla { + +class EventChainPreVisitor; + +namespace dom { + +class IDBFileHandle; + +class IDBFileRequest final : public DOMRequest, + public FileRequestBase +{ + RefPtr<IDBFileHandle> mFileHandle; + + bool mWrapAsDOMRequest; + +public: + static already_AddRefed<IDBFileRequest> + Create(nsPIDOMWindowInner* aOwner, IDBFileHandle* aFileHandle, + bool aWrapAsDOMRequest); + + // WebIDL + IDBFileHandle* + GetFileHandle() const + { + AssertIsOnOwningThread(); + return mFileHandle; + } + + IDBFileHandle* + GetLockedFile() const + { + AssertIsOnOwningThread(); + return GetFileHandle(); + } + + IMPL_EVENT_HANDLER(progress) + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBFileRequest, DOMRequest) + + // nsIDOMEventTarget + virtual nsresult + PreHandleEvent(EventChainPreVisitor& aVisitor) override; + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + // FileRequestBase + virtual FileHandleBase* + FileHandle() const override; + + virtual void + OnProgress(uint64_t aProgress, uint64_t aProgressMax) override; + + virtual void + SetResultCallback(ResultCallback* aCallback) override; + + virtual void + SetError(nsresult aError) override; + +private: + IDBFileRequest(nsPIDOMWindowInner* aWindow, + IDBFileHandle* aFileHandle, + bool aWrapAsDOMRequest); + + ~IDBFileRequest(); + + void + FireProgressEvent(uint64_t aLoaded, uint64_t aTotal); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbfilerequest_h__ diff --git a/dom/indexedDB/IDBIndex.cpp b/dom/indexedDB/IDBIndex.cpp new file mode 100644 index 000000000..657e744c9 --- /dev/null +++ b/dom/indexedDB/IDBIndex.cpp @@ -0,0 +1,679 @@ +/* -*- 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 "IDBIndex.h" + +#include "FileInfo.h" +#include "IDBCursor.h" +#include "IDBEvents.h" +#include "IDBKeyRange.h" +#include "IDBObjectStore.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { + +namespace { + +already_AddRefed<IDBRequest> +GenerateRequest(JSContext* aCx, IDBIndex* aIndex) +{ + MOZ_ASSERT(aIndex); + aIndex->AssertIsOnOwningThread(); + + IDBTransaction* transaction = aIndex->ObjectStore()->Transaction(); + + RefPtr<IDBRequest> request = + IDBRequest::Create(aCx, aIndex, transaction->Database(), transaction); + MOZ_ASSERT(request); + + return request.forget(); +} + +} // namespace + +IDBIndex::IDBIndex(IDBObjectStore* aObjectStore, const IndexMetadata* aMetadata) + : mObjectStore(aObjectStore) + , mCachedKeyPath(JS::UndefinedValue()) + , mMetadata(aMetadata) + , mId(aMetadata->id()) + , mRooted(false) +{ + MOZ_ASSERT(aObjectStore); + aObjectStore->AssertIsOnOwningThread(); + MOZ_ASSERT(aMetadata); +} + +IDBIndex::~IDBIndex() +{ + AssertIsOnOwningThread(); + + if (mRooted) { + mCachedKeyPath.setUndefined(); + mozilla::DropJSObjects(this); + } +} + +already_AddRefed<IDBIndex> +IDBIndex::Create(IDBObjectStore* aObjectStore, + const IndexMetadata& aMetadata) +{ + MOZ_ASSERT(aObjectStore); + aObjectStore->AssertIsOnOwningThread(); + + RefPtr<IDBIndex> index = new IDBIndex(aObjectStore, &aMetadata); + + return index.forget(); +} + +#ifdef DEBUG + +void +IDBIndex::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mObjectStore); + mObjectStore->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void +IDBIndex::RefreshMetadata(bool aMayDelete) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mDeletedMetadata, mMetadata == mDeletedMetadata); + + const nsTArray<IndexMetadata>& indexes = mObjectStore->Spec().indexes(); + + bool found = false; + + for (uint32_t count = indexes.Length(), index = 0; + index < count; + index++) { + const IndexMetadata& metadata = indexes[index]; + + if (metadata.id() == Id()) { + mMetadata = &metadata; + + found = true; + break; + } + } + + MOZ_ASSERT_IF(!aMayDelete && !mDeletedMetadata, found); + + if (found) { + MOZ_ASSERT(mMetadata != mDeletedMetadata); + mDeletedMetadata = nullptr; + } else { + NoteDeletion(); + } +} + +void +IDBIndex::NoteDeletion() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + MOZ_ASSERT(Id() == mMetadata->id()); + + if (mDeletedMetadata) { + MOZ_ASSERT(mMetadata == mDeletedMetadata); + return; + } + + mDeletedMetadata = new IndexMetadata(*mMetadata); + + mMetadata = mDeletedMetadata; +} + +const nsString& +IDBIndex::Name() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->name(); +} + +void +IDBIndex::SetName(const nsAString& aName, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + IDBTransaction* transaction = mObjectStore->Transaction(); + + if (transaction->GetMode() != IDBTransaction::VERSION_CHANGE || + mDeletedMetadata) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + if (aName == mMetadata->name()) { + return; + } + + // Cache logging string of this index before renaming. + const LoggingString loggingOldIndex(this); + + const int64_t indexId = Id(); + + nsresult rv = + transaction->Database()->RenameIndex(mObjectStore->Id(), + indexId, + aName); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "rename(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBIndex.rename()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + loggingOldIndex.get(), + IDB_LOG_STRINGIFY(this)); + + transaction->RenameIndex(mObjectStore, indexId, aName); +} + +bool +IDBIndex::Unique() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->unique(); +} + +bool +IDBIndex::MultiEntry() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->multiEntry(); +} + +bool +IDBIndex::LocaleAware() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->locale().IsEmpty(); +} + +const indexedDB::KeyPath& +IDBIndex::GetKeyPath() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->keyPath(); +} + +void +IDBIndex::GetLocale(nsString& aLocale) const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + if (mMetadata->locale().IsEmpty()) { + SetDOMStringToNull(aLocale); + } else { + aLocale.AssignWithConversion(mMetadata->locale()); + } +} + +const nsCString& +IDBIndex::Locale() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->locale(); +} + +bool +IDBIndex::IsAutoLocale() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->autoLocale(); +} + +nsPIDOMWindowInner* +IDBIndex::GetParentObject() const +{ + AssertIsOnOwningThread(); + + return mObjectStore->GetParentObject(); +} + +void +IDBIndex::GetKeyPath(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mCachedKeyPath.isUndefined()) { + MOZ_ASSERT(mRooted); + aResult.set(mCachedKeyPath); + return; + } + + MOZ_ASSERT(!mRooted); + + aRv = GetKeyPath().ToJSVal(aCx, mCachedKeyPath); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (mCachedKeyPath.isGCThing()) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + aResult.set(mCachedKeyPath); +} + +already_AddRefed<IDBRequest> +IDBIndex::GetInternal(bool aKeyOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedMetadata) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + IDBTransaction* transaction = mObjectStore->Transaction(); + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (!keyRange) { + // Must specify a key or keyRange for get() and getKey(). + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + const int64_t objectStoreId = mObjectStore->Id(); + const int64_t indexId = Id(); + + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + RequestParams params; + + if (aKeyOnly) { + params = IndexGetKeyParams(objectStoreId, indexId, serializedKeyRange); + } else { + params = IndexGetParams(objectStoreId, indexId, serializedKeyRange); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (aKeyOnly) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "getKey(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBIndex.getKey()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "get(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBIndex.get()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + } + + transaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBIndex::GetAllInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedMetadata) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + IDBTransaction* transaction = mObjectStore->Transaction(); + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + const int64_t objectStoreId = mObjectStore->Id(); + const int64_t indexId = Id(); + + OptionalKeyRange optionalKeyRange; + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + optionalKeyRange = serializedKeyRange; + } else { + optionalKeyRange = void_t(); + } + + const uint32_t limit = aLimit.WasPassed() ? aLimit.Value() : 0; + + RequestParams params; + if (aKeysOnly) { + params = IndexGetAllKeysParams(objectStoreId, indexId, optionalKeyRange, + limit); + } else { + params = IndexGetAllParams(objectStoreId, indexId, optionalKeyRange, limit); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (aKeysOnly) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "getAllKeys(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBIndex.getAllKeys()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(aLimit)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "getAll(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBIndex.getAll()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(aLimit)); + } + + transaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBIndex::OpenCursorInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedMetadata) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + IDBTransaction* transaction = mObjectStore->Transaction(); + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aRange, getter_AddRefs(keyRange)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + int64_t objectStoreId = mObjectStore->Id(); + int64_t indexId = Id(); + + OptionalKeyRange optionalKeyRange; + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + optionalKeyRange = Move(serializedKeyRange); + } else { + optionalKeyRange = void_t(); + } + + IDBCursor::Direction direction = IDBCursor::ConvertDirection(aDirection); + + OpenCursorParams params; + if (aKeysOnly) { + IndexOpenKeyCursorParams openParams; + openParams.objectStoreId() = objectStoreId; + openParams.indexId() = indexId; + openParams.optionalKeyRange() = Move(optionalKeyRange); + openParams.direction() = direction; + + params = Move(openParams); + } else { + IndexOpenCursorParams openParams; + openParams.objectStoreId() = objectStoreId; + openParams.indexId() = indexId; + openParams.optionalKeyRange() = Move(optionalKeyRange); + openParams.direction() = direction; + + params = Move(openParams); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (aKeysOnly) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "openKeyCursor(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBIndex.openKeyCursor()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(direction)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "openCursor(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: " + "IDBObjectStore.openKeyCursor()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(direction)); + } + + BackgroundCursorChild* actor = + new BackgroundCursorChild(request, this, direction); + + mObjectStore->Transaction()->OpenCursor(actor, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBIndex::Count(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedMetadata) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + IDBTransaction* transaction = mObjectStore->Transaction(); + if (!transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (aRv.Failed()) { + return nullptr; + } + + IndexCountParams params; + params.objectStoreId() = mObjectStore->Id(); + params.indexId() = Id(); + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + params.optionalKeyRange() = serializedKeyRange; + } else { + params.optionalKeyRange() = void_t(); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "count(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.count()", + IDB_LOG_ID_STRING(), + transaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(transaction->Database()), + IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + + transaction->StartRequest(request, params); + + return request.forget(); +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBIndex) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBIndex) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBIndex) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBIndex) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBIndex) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedKeyPath) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBIndex) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObjectStore) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBIndex) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + + // Don't unlink mObjectStore! + + tmp->mCachedKeyPath.setUndefined(); + + if (tmp->mRooted) { + mozilla::DropJSObjects(tmp); + tmp->mRooted = false; + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +JSObject* +IDBIndex::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBIndexBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBIndex.h b/dom/indexedDB/IDBIndex.h new file mode 100644 index 000000000..54b49f283 --- /dev/null +++ b/dom/indexedDB/IDBIndex.h @@ -0,0 +1,234 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbindex_h__ +#define mozilla_dom_idbindex_h__ + +#include "js/RootingAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "nsAutoPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsTArrayForwardDeclare.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class IDBObjectStore; +class IDBRequest; +template <typename> class Sequence; + +namespace indexedDB { +class IndexMetadata; +class KeyPath; +} // namespace indexedDB + +class IDBIndex final + : public nsISupports + , public nsWrapperCache +{ + RefPtr<IDBObjectStore> mObjectStore; + + JS::Heap<JS::Value> mCachedKeyPath; + + // This normally points to the IndexMetadata owned by the parent IDBDatabase + // object. However, if this index is part of a versionchange transaction and + // it gets deleted then the metadata is copied into mDeletedMetadata and + // mMetadata is set to point at mDeletedMetadata. + const indexedDB::IndexMetadata* mMetadata; + nsAutoPtr<indexedDB::IndexMetadata> mDeletedMetadata; + + const int64_t mId; + bool mRooted; + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBIndex) + + static already_AddRefed<IDBIndex> + Create(IDBObjectStore* aObjectStore, const indexedDB::IndexMetadata& aMetadata); + + int64_t + Id() const + { + AssertIsOnOwningThread(); + + return mId; + } + + const nsString& + Name() const; + + bool + Unique() const; + + bool + MultiEntry() const; + + bool + LocaleAware() const; + + const indexedDB::KeyPath& + GetKeyPath() const; + + void + GetLocale(nsString& aLocale) const; + + const nsCString& + Locale() const; + + bool + IsAutoLocale() const; + + IDBObjectStore* + ObjectStore() const + { + AssertIsOnOwningThread(); + return mObjectStore; + } + + nsPIDOMWindowInner* + GetParentObject() const; + + void + GetName(nsString& aName) const + { + aName = Name(); + } + + void + SetName(const nsAString& aName, ErrorResult& aRv); + + void + GetKeyPath(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + OpenCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ false, aCx, aRange, aDirection, + aRv); + } + + already_AddRefed<IDBRequest> + OpenKeyCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ true, aCx, aRange, aDirection, + aRv); + } + + already_AddRefed<IDBRequest> + Get(JSContext* aCx, JS::Handle<JS::Value> aKey, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ false, aCx, aKey, aRv); + } + + already_AddRefed<IDBRequest> + GetKey(JSContext* aCx, JS::Handle<JS::Value> aKey, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ true, aCx, aKey, aRv); + } + + already_AddRefed<IDBRequest> + Count(JSContext* aCx, JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + GetAll(JSContext* aCx, JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ false, aCx, aKey, aLimit, aRv); + } + + already_AddRefed<IDBRequest> + GetAllKeys(JSContext* aCx, JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ true, aCx, aKey, aLimit, aRv); + } + + void + RefreshMetadata(bool aMayDelete); + + void + NoteDeletion(); + + bool + IsDeleted() const + { + AssertIsOnOwningThread(); + + return !!mDeletedMetadata; + } + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBIndex(IDBObjectStore* aObjectStore, const indexedDB::IndexMetadata* aMetadata); + + ~IDBIndex(); + + already_AddRefed<IDBRequest> + GetInternal(bool aKeyOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + GetAllInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + OpenCursorInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbindex_h__ diff --git a/dom/indexedDB/IDBKeyRange.cpp b/dom/indexedDB/IDBKeyRange.cpp new file mode 100644 index 000000000..2de48a70c --- /dev/null +++ b/dom/indexedDB/IDBKeyRange.cpp @@ -0,0 +1,499 @@ +/* -*- 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 "IDBKeyRange.h" + +#include "Key.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/IDBKeyRangeBinding.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::indexedDB; + +namespace { + +nsresult +GetKeyFromJSVal(JSContext* aCx, + JS::Handle<JS::Value> aVal, + Key& aKey) +{ + nsresult rv = aKey.SetFromJSVal(aCx, aVal); + if (NS_FAILED(rv)) { + MOZ_ASSERT(NS_ERROR_GET_MODULE(rv) == NS_ERROR_MODULE_DOM_INDEXEDDB); + return rv; + } + + if (aKey.IsUnset()) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + return NS_OK; +} + +} // namespace + +IDBKeyRange::IDBKeyRange(nsISupports* aGlobal, + bool aLowerOpen, + bool aUpperOpen, + bool aIsOnly) + : mGlobal(aGlobal) + , mCachedLowerVal(JS::UndefinedValue()) + , mCachedUpperVal(JS::UndefinedValue()) + , mLowerOpen(aLowerOpen) + , mUpperOpen(aUpperOpen) + , mIsOnly(aIsOnly) + , mHaveCachedLowerVal(false) + , mHaveCachedUpperVal(false) + , mRooted(false) +{ +#ifdef DEBUG + mOwningThread = PR_GetCurrentThread(); +#endif + AssertIsOnOwningThread(); +} + +IDBKeyRange::~IDBKeyRange() +{ + DropJSObjects(); +} + +IDBLocaleAwareKeyRange::IDBLocaleAwareKeyRange(nsISupports* aGlobal, + bool aLowerOpen, + bool aUpperOpen, + bool aIsOnly) + : IDBKeyRange(aGlobal, aLowerOpen, aUpperOpen, aIsOnly) +{ +#ifdef DEBUG + mOwningThread = PR_GetCurrentThread(); +#endif + AssertIsOnOwningThread(); +} + +IDBLocaleAwareKeyRange::~IDBLocaleAwareKeyRange() +{ + DropJSObjects(); +} + +#ifdef DEBUG + +void +IDBKeyRange::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mOwningThread); + MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread); +} + +#endif // DEBUG + +// static +nsresult +IDBKeyRange::FromJSVal(JSContext* aCx, + JS::Handle<JS::Value> aVal, + IDBKeyRange** aKeyRange) +{ + MOZ_ASSERT_IF(!aCx, aVal.isUndefined()); + + RefPtr<IDBKeyRange> keyRange; + + if (aVal.isNullOrUndefined()) { + // undefined and null returns no IDBKeyRange. + keyRange.forget(aKeyRange); + return NS_OK; + } + + JS::Rooted<JSObject*> obj(aCx, aVal.isObject() ? &aVal.toObject() : nullptr); + bool isValidKey = aVal.isPrimitive(); + if (!isValidKey) { + js::ESClass cls; + if (!js::GetBuiltinClass(aCx, obj, &cls)) { + return NS_ERROR_UNEXPECTED; + } + isValidKey = cls == js::ESClass::Array || cls == js::ESClass::Date; + } + if (isValidKey) { + // A valid key returns an 'only' IDBKeyRange. + keyRange = new IDBKeyRange(nullptr, false, false, true); + + nsresult rv = GetKeyFromJSVal(aCx, aVal, keyRange->Lower()); + if (NS_FAILED(rv)) { + return rv; + } + } + else { + MOZ_ASSERT(aVal.isObject()); + // An object is not permitted unless it's another IDBKeyRange. + if (NS_FAILED(UNWRAP_OBJECT(IDBKeyRange, obj, keyRange))) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + } + + keyRange.forget(aKeyRange); + return NS_OK; +} + +// static +already_AddRefed<IDBKeyRange> +IDBKeyRange::FromSerialized(const SerializedKeyRange& aKeyRange) +{ + RefPtr<IDBKeyRange> keyRange = + new IDBKeyRange(nullptr, aKeyRange.lowerOpen(), aKeyRange.upperOpen(), + aKeyRange.isOnly()); + keyRange->Lower() = aKeyRange.lower(); + if (!keyRange->IsOnly()) { + keyRange->Upper() = aKeyRange.upper(); + } + return keyRange.forget(); +} + +void +IDBKeyRange::ToSerialized(SerializedKeyRange& aKeyRange) const +{ + aKeyRange.lowerOpen() = LowerOpen(); + aKeyRange.upperOpen() = UpperOpen(); + aKeyRange.isOnly() = IsOnly(); + + aKeyRange.lower() = Lower(); + if (!IsOnly()) { + aKeyRange.upper() = Upper(); + } +} + +void +IDBKeyRange::GetBindingClause(const nsACString& aKeyColumnName, + nsACString& _retval) const +{ + NS_NAMED_LITERAL_CSTRING(andStr, " AND "); + NS_NAMED_LITERAL_CSTRING(spacecolon, " :"); + NS_NAMED_LITERAL_CSTRING(lowerKey, "lower_key"); + + if (IsOnly()) { + // Both keys are set and they're equal. + _retval = andStr + aKeyColumnName + NS_LITERAL_CSTRING(" =") + + spacecolon + lowerKey; + return; + } + + nsAutoCString clause; + + if (!Lower().IsUnset()) { + // Lower key is set. + clause.Append(andStr + aKeyColumnName); + clause.AppendLiteral(" >"); + if (!LowerOpen()) { + clause.Append('='); + } + clause.Append(spacecolon + lowerKey); + } + + if (!Upper().IsUnset()) { + // Upper key is set. + clause.Append(andStr + aKeyColumnName); + clause.AppendLiteral(" <"); + if (!UpperOpen()) { + clause.Append('='); + } + clause.Append(spacecolon + NS_LITERAL_CSTRING("upper_key")); + } + + _retval = clause; +} + +nsresult +IDBKeyRange::BindToStatement(mozIStorageStatement* aStatement) const +{ + MOZ_ASSERT(aStatement); + + NS_NAMED_LITERAL_CSTRING(lowerKey, "lower_key"); + + if (IsOnly()) { + return Lower().BindToStatement(aStatement, lowerKey); + } + + nsresult rv; + + if (!Lower().IsUnset()) { + rv = Lower().BindToStatement(aStatement, lowerKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (!Upper().IsUnset()) { + rv = Upper().BindToStatement(aStatement, NS_LITERAL_CSTRING("upper_key")); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBKeyRange) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBKeyRange) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBKeyRange) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedLowerVal) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedUpperVal) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBKeyRange) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + tmp->DropJSObjects(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBKeyRange) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBKeyRange) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBKeyRange) + +NS_IMPL_ISUPPORTS_INHERITED0(IDBLocaleAwareKeyRange, IDBKeyRange) + +void +IDBKeyRange::DropJSObjects() +{ + if (!mRooted) { + return; + } + mCachedLowerVal.setUndefined(); + mCachedUpperVal.setUndefined(); + mHaveCachedLowerVal = false; + mHaveCachedUpperVal = false; + mRooted = false; + mozilla::DropJSObjects(this); +} + +bool +IDBKeyRange::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector) +{ + return IDBKeyRangeBinding::Wrap(aCx, this, aGivenProto, aReflector); +} + +bool +IDBLocaleAwareKeyRange::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector) +{ + return IDBLocaleAwareKeyRangeBinding::Wrap(aCx, this, aGivenProto, aReflector); +} + +void +IDBKeyRange::GetLower(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mHaveCachedLowerVal) { + if (!mRooted) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + aRv = Lower().ToJSVal(aCx, mCachedLowerVal); + if (aRv.Failed()) { + return; + } + + mHaveCachedLowerVal = true; + } + + aResult.set(mCachedLowerVal); +} + +void +IDBKeyRange::GetUpper(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mHaveCachedUpperVal) { + if (!mRooted) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + aRv = Upper().ToJSVal(aCx, mCachedUpperVal); + if (aRv.Failed()) { + return; + } + + mHaveCachedUpperVal = true; + } + + aResult.set(mCachedUpperVal); +} + +bool +IDBKeyRange::Includes(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) const +{ + Key key; + aRv = GetKeyFromJSVal(aCx, aValue, key); + if (aRv.Failed()) { + return false; + } + + MOZ_ASSERT(!(Lower().IsUnset() && Upper().IsUnset())); + MOZ_ASSERT_IF(IsOnly(), + !Lower().IsUnset() && !LowerOpen() && + Lower() == Upper() && LowerOpen() == UpperOpen()); + + if (!Lower().IsUnset()) { + switch (Key::CompareKeys(Lower(), key)) { + case 1: + return false; + case 0: + // Identical keys. + return !LowerOpen(); + case -1: + if (IsOnly()) { + return false; + } + break; + default: + MOZ_CRASH(); + } + } + + if (!Upper().IsUnset()) { + switch (Key::CompareKeys(key, Upper())) { + case 1: + return false; + case 0: + // Identical keys. + return !UpperOpen(); + case -1: + break; + } + } + + return true; +} + +// static +already_AddRefed<IDBKeyRange> +IDBKeyRange::Only(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) +{ + RefPtr<IDBKeyRange> keyRange = + new IDBKeyRange(aGlobal.GetAsSupports(), false, false, true); + + aRv = GetKeyFromJSVal(aGlobal.Context(), aValue, keyRange->Lower()); + if (aRv.Failed()) { + return nullptr; + } + + return keyRange.forget(); +} + +// static +already_AddRefed<IDBKeyRange> +IDBKeyRange::LowerBound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + bool aOpen, + ErrorResult& aRv) +{ + RefPtr<IDBKeyRange> keyRange = + new IDBKeyRange(aGlobal.GetAsSupports(), aOpen, true, false); + + aRv = GetKeyFromJSVal(aGlobal.Context(), aValue, keyRange->Lower()); + if (aRv.Failed()) { + return nullptr; + } + + return keyRange.forget(); +} + +// static +already_AddRefed<IDBKeyRange> +IDBKeyRange::UpperBound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + bool aOpen, + ErrorResult& aRv) +{ + RefPtr<IDBKeyRange> keyRange = + new IDBKeyRange(aGlobal.GetAsSupports(), true, aOpen, false); + + aRv = GetKeyFromJSVal(aGlobal.Context(), aValue, keyRange->Upper()); + if (aRv.Failed()) { + return nullptr; + } + + return keyRange.forget(); +} + +// static +already_AddRefed<IDBKeyRange> +IDBKeyRange::Bound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aLower, + JS::Handle<JS::Value> aUpper, + bool aLowerOpen, + bool aUpperOpen, + ErrorResult& aRv) +{ + RefPtr<IDBKeyRange> keyRange = + new IDBKeyRange(aGlobal.GetAsSupports(), aLowerOpen, aUpperOpen, false); + + aRv = GetKeyFromJSVal(aGlobal.Context(), aLower, keyRange->Lower()); + if (aRv.Failed()) { + return nullptr; + } + + aRv = GetKeyFromJSVal(aGlobal.Context(), aUpper, keyRange->Upper()); + if (aRv.Failed()) { + return nullptr; + } + + if (keyRange->Lower() > keyRange->Upper() || + (keyRange->Lower() == keyRange->Upper() && (aLowerOpen || aUpperOpen))) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + return keyRange.forget(); +} + +// static +already_AddRefed<IDBLocaleAwareKeyRange> +IDBLocaleAwareKeyRange::Bound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aLower, + JS::Handle<JS::Value> aUpper, + bool aLowerOpen, + bool aUpperOpen, + ErrorResult& aRv) +{ + RefPtr<IDBLocaleAwareKeyRange> keyRange = + new IDBLocaleAwareKeyRange(aGlobal.GetAsSupports(), aLowerOpen, aUpperOpen, false); + + aRv = GetKeyFromJSVal(aGlobal.Context(), aLower, keyRange->Lower()); + if (aRv.Failed()) { + return nullptr; + } + + aRv = GetKeyFromJSVal(aGlobal.Context(), aUpper, keyRange->Upper()); + if (aRv.Failed()) { + return nullptr; + } + + if (keyRange->Lower() == keyRange->Upper() && (aLowerOpen || aUpperOpen)) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + return keyRange.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBKeyRange.h b/dom/indexedDB/IDBKeyRange.h new file mode 100644 index 000000000..6371c0396 --- /dev/null +++ b/dom/indexedDB/IDBKeyRange.h @@ -0,0 +1,218 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbkeyrange_h__ +#define mozilla_dom_idbkeyrange_h__ + +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsString.h" + +class mozIStorageStatement; +struct PRThread; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class GlobalObject; + +namespace indexedDB { +class SerializedKeyRange; +} // namespace indexedDB + +class IDBKeyRange + : public nsISupports +{ +protected: + nsCOMPtr<nsISupports> mGlobal; + indexedDB::Key mLower; + indexedDB::Key mUpper; + JS::Heap<JS::Value> mCachedLowerVal; + JS::Heap<JS::Value> mCachedUpperVal; + + const bool mLowerOpen : 1; + const bool mUpperOpen : 1; + const bool mIsOnly : 1; + bool mHaveCachedLowerVal : 1; + bool mHaveCachedUpperVal : 1; + bool mRooted : 1; + +#ifdef DEBUG + PRThread* mOwningThread; +#endif + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBKeyRange) + + // aCx is allowed to be null, but only if aVal.isUndefined(). + static nsresult + FromJSVal(JSContext* aCx, + JS::Handle<JS::Value> aVal, + IDBKeyRange** aKeyRange); + + static already_AddRefed<IDBKeyRange> + FromSerialized(const indexedDB::SerializedKeyRange& aKeyRange); + + static already_AddRefed<IDBKeyRange> + Only(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv); + + static already_AddRefed<IDBKeyRange> + LowerBound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + bool aOpen, + ErrorResult& aRv); + + static already_AddRefed<IDBKeyRange> + UpperBound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + bool aOpen, + ErrorResult& aRv); + + static already_AddRefed<IDBKeyRange> + Bound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aLower, + JS::Handle<JS::Value> aUpper, + bool aLowerOpen, + bool aUpperOpen, + ErrorResult& aRv); + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + void + ToSerialized(indexedDB::SerializedKeyRange& aKeyRange) const; + + const indexedDB::Key& + Lower() const + { + return mLower; + } + + indexedDB::Key& + Lower() + { + return mLower; + } + + const indexedDB::Key& + Upper() const + { + return mIsOnly ? mLower : mUpper; + } + + indexedDB::Key& + Upper() + { + return mIsOnly ? mLower : mUpper; + } + + bool + Includes(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) const; + + bool + IsOnly() const + { + return mIsOnly; + } + + void + GetBindingClause(const nsACString& aKeyColumnName, + nsACString& _retval) const; + + nsresult + BindToStatement(mozIStorageStatement* aStatement) const; + + void + DropJSObjects(); + + // WebIDL + bool + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector); + + nsISupports* + GetParentObject() const + { + return mGlobal; + } + + void + GetLower(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + void + GetUpper(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + bool + LowerOpen() const + { + return mLowerOpen; + } + + bool + UpperOpen() const + { + return mUpperOpen; + } + +protected: + IDBKeyRange(nsISupports* aGlobal, + bool aLowerOpen, + bool aUpperOpen, + bool aIsOnly); + + virtual ~IDBKeyRange(); +}; + +class IDBLocaleAwareKeyRange final + : public IDBKeyRange +{ + IDBLocaleAwareKeyRange(nsISupports* aGlobal, + bool aLowerOpen, + bool aUpperOpen, + bool aIsOnly); + + ~IDBLocaleAwareKeyRange(); + +public: + static already_AddRefed<IDBLocaleAwareKeyRange> + Bound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aLower, + JS::Handle<JS::Value> aUpper, + bool aLowerOpen, + bool aUpperOpen, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + + // WebIDL + bool + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbkeyrange_h__ diff --git a/dom/indexedDB/IDBMutableFile.cpp b/dom/indexedDB/IDBMutableFile.cpp new file mode 100644 index 000000000..9e9bfc4ee --- /dev/null +++ b/dom/indexedDB/IDBMutableFile.cpp @@ -0,0 +1,283 @@ +/* -*- 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 "IDBMutableFile.h" + +#include "ActorsChild.h" +#include "FileInfo.h" +#include "FileSnapshot.h" +#include "IDBDatabase.h" +#include "IDBFactory.h" +#include "IDBFileHandle.h" +#include "IDBFileRequest.h" +#include "IndexedDatabaseManager.h" +#include "MainThreadUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IDBMutableFileBinding.h" +#include "mozilla/dom/filehandle/ActorsChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/quota/FileStreams.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIInputStream.h" +#include "nsIPrincipal.h" +#include "ReportInternalError.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::dom::quota; + +IDBMutableFile::IDBMutableFile(IDBDatabase* aDatabase, + BackgroundMutableFileChild* aActor, + const nsAString& aName, + const nsAString& aType) + : DOMEventTargetHelper(aDatabase) + , MutableFileBase(DEBUGONLY(aDatabase->OwningThread(),) + aActor) + , mDatabase(aDatabase) + , mName(aName) + , mType(aType) + , mInvalidated(false) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + mDatabase->NoteLiveMutableFile(this); +} + +IDBMutableFile::~IDBMutableFile() +{ + AssertIsOnOwningThread(); + + mDatabase->NoteFinishedMutableFile(this); +} + +int64_t +IDBMutableFile::GetFileId() const +{ + AssertIsOnOwningThread(); + + int64_t fileId; + if (!mBackgroundActor || + NS_WARN_IF(!mBackgroundActor->SendGetFileId(&fileId))) { + return -1; + } + + return fileId; +} + +void +IDBMutableFile::Invalidate() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mInvalidated); + + mInvalidated = true; + + AbortFileHandles(); +} + +void +IDBMutableFile::RegisterFileHandle(IDBFileHandle* aFileHandle) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aFileHandle); + aFileHandle->AssertIsOnOwningThread(); + MOZ_ASSERT(!mFileHandles.Contains(aFileHandle)); + + mFileHandles.PutEntry(aFileHandle); +} + +void +IDBMutableFile::UnregisterFileHandle(IDBFileHandle* aFileHandle) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aFileHandle); + aFileHandle->AssertIsOnOwningThread(); + MOZ_ASSERT(mFileHandles.Contains(aFileHandle)); + + mFileHandles.RemoveEntry(aFileHandle); +} + +void +IDBMutableFile::AbortFileHandles() +{ + AssertIsOnOwningThread(); + + class MOZ_STACK_CLASS Helper final + { + public: + static void + AbortFileHandles(nsTHashtable<nsPtrHashKey<IDBFileHandle>>& aTable) + { + if (!aTable.Count()) { + return; + } + + nsTArray<RefPtr<IDBFileHandle>> fileHandlesToAbort; + fileHandlesToAbort.SetCapacity(aTable.Count()); + + for (auto iter = aTable.Iter(); !iter.Done(); iter.Next()) { + IDBFileHandle* fileHandle = iter.Get()->GetKey(); + MOZ_ASSERT(fileHandle); + + fileHandle->AssertIsOnOwningThread(); + + if (!fileHandle->IsDone()) { + fileHandlesToAbort.AppendElement(iter.Get()->GetKey()); + } + } + MOZ_ASSERT(fileHandlesToAbort.Length() <= aTable.Count()); + + if (fileHandlesToAbort.IsEmpty()) { + return; + } + + for (RefPtr<IDBFileHandle>& fileHandle : fileHandlesToAbort) { + MOZ_ASSERT(fileHandle); + + fileHandle->Abort(); + } + } + }; + + Helper::AbortFileHandles(mFileHandles); +} + +IDBDatabase* +IDBMutableFile::Database() const +{ + AssertIsOnOwningThread(); + + return mDatabase; +} + +already_AddRefed<IDBFileHandle> +IDBMutableFile::Open(FileMode aMode, ErrorResult& aError) +{ + AssertIsOnOwningThread(); + + if (QuotaManager::IsShuttingDown() || + mDatabase->IsClosed() || + !GetOwner()) { + aError.Throw(NS_ERROR_DOM_FILEHANDLE_UNKNOWN_ERR); + return nullptr; + } + + RefPtr<IDBFileHandle> fileHandle = + IDBFileHandle::Create(this, aMode); + if (NS_WARN_IF(!fileHandle)) { + aError.Throw(NS_ERROR_DOM_FILEHANDLE_UNKNOWN_ERR); + return nullptr; + } + + BackgroundFileHandleChild* actor = + new BackgroundFileHandleChild(DEBUGONLY(mBackgroundActor->OwningThread(),) + fileHandle); + + MOZ_ALWAYS_TRUE( + mBackgroundActor->SendPBackgroundFileHandleConstructor(actor, aMode)); + + fileHandle->SetBackgroundActor(actor); + + return fileHandle.forget(); +} + +already_AddRefed<DOMRequest> +IDBMutableFile::GetFile(ErrorResult& aError) +{ + RefPtr<IDBFileHandle> fileHandle = Open(FileMode::Readonly, aError); + if (NS_WARN_IF(aError.Failed())) { + return nullptr; + } + + FileRequestGetFileParams params; + + RefPtr<IDBFileRequest> request = + IDBFileRequest::Create(GetOwner(), + fileHandle, + /* aWrapAsDOMRequest */ true); + + fileHandle->StartRequest(request, params); + + return request.forget(); +} + +NS_IMPL_ADDREF_INHERITED(IDBMutableFile, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(IDBMutableFile, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBMutableFile) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBMutableFile) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBMutableFile, + DOMEventTargetHelper) + tmp->AssertIsOnOwningThread(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDatabase) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBMutableFile, + DOMEventTargetHelper) + tmp->AssertIsOnOwningThread(); + + // Don't unlink mDatabase! +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +JSObject* +IDBMutableFile::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBMutableFileBinding::Wrap(aCx, this, aGivenProto); +} + +const nsString& +IDBMutableFile::Name() const +{ + AssertIsOnOwningThread(); + + return mName; +} + +const nsString& +IDBMutableFile::Type() const +{ + AssertIsOnOwningThread(); + + return mType; +} + +bool +IDBMutableFile::IsInvalidated() +{ + AssertIsOnOwningThread(); + + return mInvalidated; +} + +already_AddRefed<File> +IDBMutableFile::CreateFileFor(BlobImpl* aBlobImpl, + FileHandleBase* aFileHandle) +{ + AssertIsOnOwningThread(); + + RefPtr<BlobImpl> blobImplSnapshot = + new BlobImplSnapshot(aBlobImpl, static_cast<IDBFileHandle*>(aFileHandle)); + + RefPtr<File> file = File::Create(GetOwner(), blobImplSnapshot); + return file.forget(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBMutableFile.h b/dom/indexedDB/IDBMutableFile.h new file mode 100644 index 000000000..3e328d366 --- /dev/null +++ b/dom/indexedDB/IDBMutableFile.h @@ -0,0 +1,139 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbmutablefile_h__ +#define mozilla_dom_idbmutablefile_h__ + +#include "js/TypeDecls.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/FileModeBinding.h" +#include "mozilla/dom/MutableFileBase.h" +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include "nsString.h" +#include "nsTHashtable.h" + +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class DOMRequest; +class File; +class IDBDatabase; +class IDBFileHandle; + +namespace indexedDB { +class BackgroundMutableFileChild; +} + +class IDBMutableFile final + : public DOMEventTargetHelper + , public MutableFileBase +{ + RefPtr<IDBDatabase> mDatabase; + + nsTHashtable<nsPtrHashKey<IDBFileHandle>> mFileHandles; + + nsString mName; + nsString mType; + + Atomic<bool> mInvalidated; + +public: + IDBMutableFile(IDBDatabase* aDatabase, + indexedDB::BackgroundMutableFileChild* aActor, + const nsAString& aName, + const nsAString& aType); + + void + SetLazyData(const nsAString& aName, + const nsAString& aType) + { + mName = aName; + mType = aType; + } + + int64_t + GetFileId() const; + + void + Invalidate(); + + void + RegisterFileHandle(IDBFileHandle* aFileHandle); + + void + UnregisterFileHandle(IDBFileHandle* aFileHandle); + + void + AbortFileHandles(); + + // WebIDL + nsPIDOMWindowInner* + GetParentObject() const + { + return GetOwner(); + } + + void + GetName(nsString& aName) const + { + aName = mName; + } + + void + GetType(nsString& aType) const + { + aType = mType; + } + + IDBDatabase* + Database() const; + + already_AddRefed<IDBFileHandle> + Open(FileMode aMode, ErrorResult& aError); + + already_AddRefed<DOMRequest> + GetFile(ErrorResult& aError); + + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(error) + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBMutableFile, DOMEventTargetHelper) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + // MutableFileBase + virtual const nsString& + Name() const override; + + virtual const nsString& + Type() const override; + + virtual bool + IsInvalidated() override; + + virtual already_AddRefed<File> + CreateFileFor(BlobImpl* aBlobImpl, + FileHandleBase* aFileHandle) override; + +private: + ~IDBMutableFile(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbmutablefile_h__ diff --git a/dom/indexedDB/IDBObjectStore.cpp b/dom/indexedDB/IDBObjectStore.cpp new file mode 100644 index 000000000..c9ab24970 --- /dev/null +++ b/dom/indexedDB/IDBObjectStore.cpp @@ -0,0 +1,2503 @@ +/* -*- 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 "IDBObjectStore.h" + +#include "FileInfo.h" +#include "IDBCursor.h" +#include "IDBDatabase.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBKeyRange.h" +#include "IDBMutableFile.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDatabaseManager.h" +#include "js/Class.h" +#include "js/Date.h" +#include "js/StructuredClone.h" +#include "KeyPath.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Move.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IDBMutableFileBinding.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/IDBObjectStoreBinding.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/StructuredCloneTags.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/ipc/BlobChild.h" +#include "mozilla/dom/ipc/BlobParent.h" +#include "mozilla/dom/ipc/nsIRemoteBlob.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCOMPtr.h" +#include "nsQueryObject.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "WorkerPrivate.h" +#include "WorkerScope.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::quota; +using namespace mozilla::dom::workers; +using namespace mozilla::ipc; + +struct IDBObjectStore::StructuredCloneWriteInfo +{ + JSAutoStructuredCloneBuffer mCloneBuffer; + nsTArray<StructuredCloneFile> mFiles; + IDBDatabase* mDatabase; + uint64_t mOffsetToKeyProp; + + explicit StructuredCloneWriteInfo(IDBDatabase* aDatabase) + : mCloneBuffer(JS::StructuredCloneScope::SameProcessSameThread, nullptr, + nullptr) + , mDatabase(aDatabase) + , mOffsetToKeyProp(0) + { + MOZ_ASSERT(aDatabase); + + MOZ_COUNT_CTOR(StructuredCloneWriteInfo); + } + + StructuredCloneWriteInfo(StructuredCloneWriteInfo&& aCloneWriteInfo) + : mCloneBuffer(Move(aCloneWriteInfo.mCloneBuffer)) + , mDatabase(aCloneWriteInfo.mDatabase) + , mOffsetToKeyProp(aCloneWriteInfo.mOffsetToKeyProp) + { + MOZ_ASSERT(mDatabase); + + MOZ_COUNT_CTOR(StructuredCloneWriteInfo); + + mFiles.SwapElements(aCloneWriteInfo.mFiles); + aCloneWriteInfo.mOffsetToKeyProp = 0; + } + + ~StructuredCloneWriteInfo() + { + MOZ_COUNT_DTOR(StructuredCloneWriteInfo); + } +}; + +namespace { + +struct MOZ_STACK_CLASS MutableFileData final +{ + nsString type; + nsString name; + + MutableFileData() + { + MOZ_COUNT_CTOR(MutableFileData); + } + + ~MutableFileData() + { + MOZ_COUNT_DTOR(MutableFileData); + } +}; + +struct MOZ_STACK_CLASS BlobOrFileData final +{ + uint32_t tag; + uint64_t size; + nsString type; + nsString name; + int64_t lastModifiedDate; + + BlobOrFileData() + : tag(0) + , size(0) + , lastModifiedDate(INT64_MAX) + { + MOZ_COUNT_CTOR(BlobOrFileData); + } + + ~BlobOrFileData() + { + MOZ_COUNT_DTOR(BlobOrFileData); + } +}; + +struct MOZ_STACK_CLASS WasmModuleData final +{ + uint32_t bytecodeIndex; + uint32_t compiledIndex; + uint32_t flags; + + explicit WasmModuleData(uint32_t aFlags) + : bytecodeIndex(0) + , compiledIndex(0) + , flags(aFlags) + { + MOZ_COUNT_CTOR(WasmModuleData); + } + + ~WasmModuleData() + { + MOZ_COUNT_DTOR(WasmModuleData); + } +}; + +struct MOZ_STACK_CLASS GetAddInfoClosure final +{ + IDBObjectStore::StructuredCloneWriteInfo& mCloneWriteInfo; + JS::Handle<JS::Value> mValue; + + GetAddInfoClosure(IDBObjectStore::StructuredCloneWriteInfo& aCloneWriteInfo, + JS::Handle<JS::Value> aValue) + : mCloneWriteInfo(aCloneWriteInfo) + , mValue(aValue) + { + MOZ_COUNT_CTOR(GetAddInfoClosure); + } + + ~GetAddInfoClosure() + { + MOZ_COUNT_DTOR(GetAddInfoClosure); + } +}; + +already_AddRefed<IDBRequest> +GenerateRequest(JSContext* aCx, IDBObjectStore* aObjectStore) +{ + MOZ_ASSERT(aObjectStore); + aObjectStore->AssertIsOnOwningThread(); + + IDBTransaction* transaction = aObjectStore->Transaction(); + + RefPtr<IDBRequest> request = + IDBRequest::Create(aCx, aObjectStore, transaction->Database(), transaction); + MOZ_ASSERT(request); + + return request.forget(); +} + +bool +StructuredCloneWriteCallback(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle<JSObject*> aObj, + void* aClosure) +{ + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWriter); + MOZ_ASSERT(aClosure); + + auto* cloneWriteInfo = + static_cast<IDBObjectStore::StructuredCloneWriteInfo*>(aClosure); + + if (JS_GetClass(aObj) == IDBObjectStore::DummyPropClass()) { + MOZ_ASSERT(!cloneWriteInfo->mOffsetToKeyProp); + cloneWriteInfo->mOffsetToKeyProp = js::GetSCOffset(aWriter); + + uint64_t value = 0; + // Omit endian swap + return JS_WriteBytes(aWriter, &value, sizeof(value)); + } + + // UNWRAP_OBJECT calls might mutate this. + JS::Rooted<JSObject*> obj(aCx, aObj); + + IDBMutableFile* mutableFile; + if (NS_SUCCEEDED(UNWRAP_OBJECT(IDBMutableFile, &obj, mutableFile))) { + if (cloneWriteInfo->mDatabase->IsFileHandleDisabled()) { + return false; + } + + IDBDatabase* database = mutableFile->Database(); + MOZ_ASSERT(database); + + // Throw when trying to store IDBMutableFile objects that live in a + // different database. + if (database != cloneWriteInfo->mDatabase) { + MOZ_ASSERT(!SameCOMIdentity(database, cloneWriteInfo->mDatabase)); + + if (database->Name() != cloneWriteInfo->mDatabase->Name()) { + return false; + } + + nsCString fileOrigin, databaseOrigin; + PersistenceType filePersistenceType, databasePersistenceType; + + if (NS_WARN_IF(NS_FAILED(database->GetQuotaInfo(fileOrigin, + &filePersistenceType)))) { + return false; + } + + if (NS_WARN_IF(NS_FAILED(cloneWriteInfo->mDatabase->GetQuotaInfo( + databaseOrigin, + &databasePersistenceType)))) { + return false; + } + + if (filePersistenceType != databasePersistenceType || + fileOrigin != databaseOrigin) { + return false; + } + } + + if (cloneWriteInfo->mFiles.Length() > size_t(UINT32_MAX)) { + MOZ_ASSERT(false, "Fix the structured clone data to use a bigger type!"); + return false; + } + + const uint32_t index = cloneWriteInfo->mFiles.Length(); + + NS_ConvertUTF16toUTF8 convType(mutableFile->Type()); + uint32_t convTypeLength = + NativeEndian::swapToLittleEndian(convType.Length()); + + NS_ConvertUTF16toUTF8 convName(mutableFile->Name()); + uint32_t convNameLength = + NativeEndian::swapToLittleEndian(convName.Length()); + + if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_MUTABLEFILE, uint32_t(index)) || + !JS_WriteBytes(aWriter, &convTypeLength, sizeof(uint32_t)) || + !JS_WriteBytes(aWriter, convType.get(), convType.Length()) || + !JS_WriteBytes(aWriter, &convNameLength, sizeof(uint32_t)) || + !JS_WriteBytes(aWriter, convName.get(), convName.Length())) { + return false; + } + + StructuredCloneFile* newFile = cloneWriteInfo->mFiles.AppendElement(); + newFile->mMutableFile = mutableFile; + newFile->mType = StructuredCloneFile::eMutableFile; + + return true; + } + + { + Blob* blob = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { + ErrorResult rv; + uint64_t size = blob->GetSize(rv); + MOZ_ASSERT(!rv.Failed()); + + size = NativeEndian::swapToLittleEndian(size); + + nsString type; + blob->GetType(type); + + NS_ConvertUTF16toUTF8 convType(type); + uint32_t convTypeLength = + NativeEndian::swapToLittleEndian(convType.Length()); + + if (cloneWriteInfo->mFiles.Length() > size_t(UINT32_MAX)) { + MOZ_ASSERT(false, + "Fix the structured clone data to use a bigger type!"); + return false; + } + + const uint32_t index = cloneWriteInfo->mFiles.Length(); + + if (!JS_WriteUint32Pair(aWriter, + blob->IsFile() ? SCTAG_DOM_FILE : SCTAG_DOM_BLOB, + index) || + !JS_WriteBytes(aWriter, &size, sizeof(size)) || + !JS_WriteBytes(aWriter, &convTypeLength, sizeof(convTypeLength)) || + !JS_WriteBytes(aWriter, convType.get(), convType.Length())) { + return false; + } + + RefPtr<File> file = blob->ToFile(); + if (file) { + ErrorResult rv; + int64_t lastModifiedDate = file->GetLastModified(rv); + MOZ_ALWAYS_TRUE(!rv.Failed()); + + lastModifiedDate = NativeEndian::swapToLittleEndian(lastModifiedDate); + + nsString name; + file->GetName(name); + + NS_ConvertUTF16toUTF8 convName(name); + uint32_t convNameLength = + NativeEndian::swapToLittleEndian(convName.Length()); + + if (!JS_WriteBytes(aWriter, &lastModifiedDate, sizeof(lastModifiedDate)) || + !JS_WriteBytes(aWriter, &convNameLength, sizeof(convNameLength)) || + !JS_WriteBytes(aWriter, convName.get(), convName.Length())) { + return false; + } + } + + StructuredCloneFile* newFile = cloneWriteInfo->mFiles.AppendElement(); + newFile->mBlob = blob; + newFile->mType = StructuredCloneFile::eBlob; + + return true; + } + } + + if (JS::IsWasmModuleObject(aObj)) { + RefPtr<JS::WasmModule> module = JS::GetWasmModule(aObj); + MOZ_ASSERT(module); + + size_t bytecodeSize; + size_t compiledSize; + module->serializedSize(&bytecodeSize, &compiledSize); + + UniquePtr<uint8_t[]> bytecode(new uint8_t[bytecodeSize]); + MOZ_ASSERT(bytecode); + + UniquePtr<uint8_t[]> compiled(new uint8_t[compiledSize]); + MOZ_ASSERT(compiled); + + module->serialize(bytecode.get(), + bytecodeSize, + compiled.get(), + compiledSize); + + RefPtr<BlobImpl> blobImpl = + new BlobImplMemory(bytecode.release(), bytecodeSize, EmptyString()); + RefPtr<Blob> bytecodeBlob = Blob::Create(nullptr, blobImpl); + + blobImpl = + new BlobImplMemory(compiled.release(), compiledSize, EmptyString()); + RefPtr<Blob> compiledBlob = Blob::Create(nullptr, blobImpl); + + if (cloneWriteInfo->mFiles.Length() + 1 > size_t(UINT32_MAX)) { + MOZ_ASSERT(false, "Fix the structured clone data to use a bigger type!"); + return false; + } + + const uint32_t index = cloneWriteInfo->mFiles.Length(); + + // The ordering of the bytecode and compiled file is significant and must + // never be changed. These two files must always form a pair + // [eWasmBytecode, eWasmCompiled]. Everything else depends on it! + if (!JS_WriteUint32Pair(aWriter, SCTAG_DOM_WASM, /* flags */ 0) || + !JS_WriteUint32Pair(aWriter, index, index + 1)) { + return false; + } + + StructuredCloneFile* newFile = cloneWriteInfo->mFiles.AppendElement(); + newFile->mBlob = bytecodeBlob; + newFile->mType = StructuredCloneFile::eWasmBytecode; + + newFile = cloneWriteInfo->mFiles.AppendElement(); + newFile->mBlob = compiledBlob; + newFile->mType = StructuredCloneFile::eWasmCompiled; + + return true; + } + + return StructuredCloneHolder::WriteFullySerializableObjects(aCx, aWriter, aObj); +} + +nsresult +GetAddInfoCallback(JSContext* aCx, void* aClosure) +{ + static const JSStructuredCloneCallbacks kStructuredCloneCallbacks = { + nullptr /* read */, + StructuredCloneWriteCallback /* write */, + nullptr /* reportError */, + nullptr /* readTransfer */, + nullptr /* writeTransfer */, + nullptr /* freeTransfer */ + }; + + MOZ_ASSERT(aCx); + + auto* data = static_cast<GetAddInfoClosure*>(aClosure); + MOZ_ASSERT(data); + + data->mCloneWriteInfo.mOffsetToKeyProp = 0; + + if (!data->mCloneWriteInfo.mCloneBuffer.write(aCx, + data->mValue, + &kStructuredCloneCallbacks, + &data->mCloneWriteInfo)) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + return NS_OK; +} + +BlobChild* +ActorFromRemoteBlobImpl(BlobImpl* aImpl) +{ + MOZ_ASSERT(aImpl); + + nsCOMPtr<nsIRemoteBlob> remoteBlob = do_QueryInterface(aImpl); + if (remoteBlob) { + BlobChild* actor = remoteBlob->GetBlobChild(); + MOZ_ASSERT(actor); + + if (actor->GetContentManager()) { + return nullptr; + } + + MOZ_ASSERT(actor->GetBackgroundManager()); + MOZ_ASSERT(BackgroundChild::GetForCurrentThread()); + MOZ_ASSERT(actor->GetBackgroundManager() == + BackgroundChild::GetForCurrentThread(), + "Blob actor is not bound to this thread!"); + + return actor; + } + + return nullptr; +} + +bool +ResolveMysteryMutableFile(IDBMutableFile* aMutableFile, + const nsString& aName, + const nsString& aType) +{ + MOZ_ASSERT(aMutableFile); + aMutableFile->SetLazyData(aName, aType); + return true; +} + +bool +ResolveMysteryFile(BlobImpl* aImpl, + const nsString& aName, + const nsString& aContentType, + uint64_t aSize, + uint64_t aLastModifiedDate) +{ + BlobChild* actor = ActorFromRemoteBlobImpl(aImpl); + if (actor) { + return actor->SetMysteryBlobInfo(aName, aContentType, + aSize, aLastModifiedDate); + } + return true; +} + +bool +ResolveMysteryBlob(BlobImpl* aImpl, + const nsString& aContentType, + uint64_t aSize) +{ + BlobChild* actor = ActorFromRemoteBlobImpl(aImpl); + if (actor) { + return actor->SetMysteryBlobInfo(aContentType, aSize); + } + return true; +} + +bool +StructuredCloneReadString(JSStructuredCloneReader* aReader, + nsCString& aString) +{ + uint32_t length; + if (!JS_ReadBytes(aReader, &length, sizeof(uint32_t))) { + NS_WARNING("Failed to read length!"); + return false; + } + length = NativeEndian::swapFromLittleEndian(length); + + if (!aString.SetLength(length, fallible)) { + NS_WARNING("Out of memory?"); + return false; + } + char* buffer = aString.BeginWriting(); + + if (!JS_ReadBytes(aReader, buffer, length)) { + NS_WARNING("Failed to read type!"); + return false; + } + + return true; +} + +bool +ReadFileHandle(JSStructuredCloneReader* aReader, + MutableFileData* aRetval) +{ + static_assert(SCTAG_DOM_MUTABLEFILE == 0xFFFF8004, "Update me!"); + MOZ_ASSERT(aReader && aRetval); + + nsCString type; + if (!StructuredCloneReadString(aReader, type)) { + return false; + } + CopyUTF8toUTF16(type, aRetval->type); + + nsCString name; + if (!StructuredCloneReadString(aReader, name)) { + return false; + } + CopyUTF8toUTF16(name, aRetval->name); + + return true; +} + +bool +ReadBlobOrFile(JSStructuredCloneReader* aReader, + uint32_t aTag, + BlobOrFileData* aRetval) +{ + static_assert(SCTAG_DOM_BLOB == 0xffff8001 && + SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE == 0xffff8002 && + SCTAG_DOM_FILE == 0xffff8005, + "Update me!"); + + MOZ_ASSERT(aReader); + MOZ_ASSERT(aTag == SCTAG_DOM_FILE || + aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE || + aTag == SCTAG_DOM_BLOB); + MOZ_ASSERT(aRetval); + + aRetval->tag = aTag; + + uint64_t size; + if (NS_WARN_IF(!JS_ReadBytes(aReader, &size, sizeof(uint64_t)))) { + return false; + } + + aRetval->size = NativeEndian::swapFromLittleEndian(size); + + nsCString type; + if (NS_WARN_IF(!StructuredCloneReadString(aReader, type))) { + return false; + } + + CopyUTF8toUTF16(type, aRetval->type); + + // Blobs are done. + if (aTag == SCTAG_DOM_BLOB) { + return true; + } + + MOZ_ASSERT(aTag == SCTAG_DOM_FILE || + aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE); + + int64_t lastModifiedDate; + if (aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE) { + lastModifiedDate = INT64_MAX; + } else { + if (NS_WARN_IF(!JS_ReadBytes(aReader, &lastModifiedDate, + sizeof(lastModifiedDate)))) { + return false; + } + lastModifiedDate = NativeEndian::swapFromLittleEndian(lastModifiedDate); + } + + aRetval->lastModifiedDate = lastModifiedDate; + + nsCString name; + if (NS_WARN_IF(!StructuredCloneReadString(aReader, name))) { + return false; + } + + CopyUTF8toUTF16(name, aRetval->name); + + return true; +} + +bool +ReadWasmModule(JSStructuredCloneReader* aReader, + WasmModuleData* aRetval) +{ + static_assert(SCTAG_DOM_WASM == 0xFFFF8006, "Update me!"); + MOZ_ASSERT(aReader && aRetval); + + uint32_t bytecodeIndex; + uint32_t compiledIndex; + if (NS_WARN_IF(!JS_ReadUint32Pair(aReader, + &bytecodeIndex, + &compiledIndex))) { + return false; + } + + aRetval->bytecodeIndex = bytecodeIndex; + aRetval->compiledIndex = compiledIndex; + + return true; +} + +class ValueDeserializationHelper +{ +public: + static bool + CreateAndWrapMutableFile(JSContext* aCx, + StructuredCloneFile& aFile, + const MutableFileData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.mType == StructuredCloneFile::eMutableFile); + + if (!aFile.mMutableFile || !NS_IsMainThread()) { + return false; + } + + if (NS_WARN_IF(!ResolveMysteryMutableFile(aFile.mMutableFile, + aData.name, + aData.type))) { + return false; + } + + JS::Rooted<JS::Value> wrappedMutableFile(aCx); + if (!ToJSValue(aCx, aFile.mMutableFile, &wrappedMutableFile)) { + return false; + } + + aResult.set(&wrappedMutableFile.toObject()); + return true; + } + + static bool + CreateAndWrapBlobOrFile(JSContext* aCx, + IDBDatabase* aDatabase, + StructuredCloneFile& aFile, + const BlobOrFileData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aData.tag == SCTAG_DOM_FILE || + aData.tag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE || + aData.tag == SCTAG_DOM_BLOB); + MOZ_ASSERT(aFile.mType == StructuredCloneFile::eBlob); + MOZ_ASSERT(aFile.mBlob); + + // It can happen that this IDB is chrome code, so there is no parent, but + // still we want to set a correct parent for the new File object. + nsCOMPtr<nsISupports> parent; + if (NS_IsMainThread()) { + if (aDatabase && aDatabase->GetParentObject()) { + parent = aDatabase->GetParentObject(); + } else { + parent = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx)); + } + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + WorkerGlobalScope* globalScope = workerPrivate->GlobalScope(); + MOZ_ASSERT(globalScope); + + parent = do_QueryObject(globalScope); + } + + MOZ_ASSERT(parent); + + if (aData.tag == SCTAG_DOM_BLOB) { + if (NS_WARN_IF(!ResolveMysteryBlob(aFile.mBlob->Impl(), + aData.type, + aData.size))) { + return false; + } + + MOZ_ASSERT(!aFile.mBlob->IsFile()); + + JS::Rooted<JS::Value> wrappedBlob(aCx); + if (!ToJSValue(aCx, aFile.mBlob, &wrappedBlob)) { + return false; + } + + aResult.set(&wrappedBlob.toObject()); + return true; + } + + if (NS_WARN_IF(!ResolveMysteryFile(aFile.mBlob->Impl(), + aData.name, + aData.type, + aData.size, + aData.lastModifiedDate))) { + return false; + } + + MOZ_ASSERT(aFile.mBlob->IsFile()); + RefPtr<File> file = aFile.mBlob->ToFile(); + MOZ_ASSERT(file); + + JS::Rooted<JS::Value> wrappedFile(aCx); + if (!ToJSValue(aCx, file, &wrappedFile)) { + return false; + } + + aResult.set(&wrappedFile.toObject()); + return true; + } + + static bool + CreateAndWrapWasmModule(JSContext* aCx, + StructuredCloneFile& aFile, + const WasmModuleData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.mType == StructuredCloneFile::eWasmCompiled); + MOZ_ASSERT(!aFile.mBlob); + MOZ_ASSERT(aFile.mWasmModule); + + JS::Rooted<JSObject*> moduleObj(aCx, aFile.mWasmModule->createObject(aCx)); + if (NS_WARN_IF(!moduleObj)) { + return false; + } + + aResult.set(moduleObj); + return true; + } +}; + +class IndexDeserializationHelper +{ +public: + static bool + CreateAndWrapMutableFile(JSContext* aCx, + StructuredCloneFile& aFile, + const MutableFileData& aData, + JS::MutableHandle<JSObject*> aResult) + { + // MutableFile can't be used in index creation, so just make a dummy object. + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (NS_WARN_IF(!obj)) { + return false; + } + + aResult.set(obj); + return true; + } + + static bool + CreateAndWrapBlobOrFile(JSContext* aCx, + IDBDatabase* aDatabase, + StructuredCloneFile& aFile, + const BlobOrFileData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aData.tag == SCTAG_DOM_FILE || + aData.tag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE || + aData.tag == SCTAG_DOM_BLOB); + + // The following properties are available for use in index creation + // Blob.size + // Blob.type + // File.name + // File.lastModifiedDate + + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (NS_WARN_IF(!obj)) { + return false; + } + + // Technically these props go on the proto, but this detail won't change + // the results of index creation. + + JS::Rooted<JSString*> type(aCx, + JS_NewUCStringCopyN(aCx, aData.type.get(), aData.type.Length())); + if (NS_WARN_IF(!type)) { + return false; + } + + if (NS_WARN_IF(!JS_DefineProperty(aCx, + obj, + "size", + double(aData.size), + 0))) { + return false; + } + + if (NS_WARN_IF(!JS_DefineProperty(aCx, obj, "type", type, 0))) { + return false; + } + + if (aData.tag == SCTAG_DOM_BLOB) { + aResult.set(obj); + return true; + } + + JS::Rooted<JSString*> name(aCx, + JS_NewUCStringCopyN(aCx, aData.name.get(), aData.name.Length())); + if (NS_WARN_IF(!name)) { + return false; + } + + JS::ClippedTime time = JS::TimeClip(aData.lastModifiedDate); + JS::Rooted<JSObject*> date(aCx, JS::NewDateObject(aCx, time)); + if (NS_WARN_IF(!date)) { + return false; + } + + if (NS_WARN_IF(!JS_DefineProperty(aCx, obj, "name", name, 0))) { + return false; + } + + if (NS_WARN_IF(!JS_DefineProperty(aCx, obj, "lastModifiedDate", date, 0))) { + return false; + } + + aResult.set(obj); + return true; + } + + static bool + CreateAndWrapWasmModule(JSContext* aCx, + StructuredCloneFile& aFile, + const WasmModuleData& aData, + JS::MutableHandle<JSObject*> aResult) + { + // Wasm module can't be used in index creation, so just make a dummy object. + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (NS_WARN_IF(!obj)) { + return false; + } + + aResult.set(obj); + return true; + } +}; + +// We don't need to upgrade database on B2G. See the comment in ActorsParent.cpp, +// UpgradeSchemaFrom18_0To19_0() +#if !defined(MOZ_B2G) + +class UpgradeDeserializationHelper +{ +public: + static bool + CreateAndWrapMutableFile(JSContext* aCx, + StructuredCloneFile& aFile, + const MutableFileData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.mType == StructuredCloneFile::eBlob); + + aFile.mType = StructuredCloneFile::eMutableFile; + + // Just make a dummy object. The file_ids upgrade function is only + // interested in the |mType| flag. + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + + if (NS_WARN_IF(!obj)) { + return false; + } + + aResult.set(obj); + return true; + } + + static bool + CreateAndWrapBlobOrFile(JSContext* aCx, + IDBDatabase* aDatabase, + StructuredCloneFile& aFile, + const BlobOrFileData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.mType == StructuredCloneFile::eBlob); + MOZ_ASSERT(aData.tag == SCTAG_DOM_FILE || + aData.tag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE || + aData.tag == SCTAG_DOM_BLOB); + + // Just make a dummy object. The file_ids upgrade function is only interested + // in the |mType| flag. + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + + if (NS_WARN_IF(!obj)) { + return false; + } + + aResult.set(obj); + return true; + } + + static bool + CreateAndWrapWasmModule(JSContext* aCx, + StructuredCloneFile& aFile, + const WasmModuleData& aData, + JS::MutableHandle<JSObject*> aResult) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.mType == StructuredCloneFile::eBlob); + + MOZ_ASSERT(false, "This should never be possible!"); + + return false; + } +}; + +#endif // MOZ_B2G + +template <class Traits> +JSObject* +CommonStructuredCloneReadCallback(JSContext* aCx, + JSStructuredCloneReader* aReader, + uint32_t aTag, + uint32_t aData, + void* aClosure) +{ + // We need to statically assert that our tag values are what we expect + // so that if people accidentally change them they notice. + static_assert(SCTAG_DOM_BLOB == 0xffff8001 && + SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE == 0xffff8002 && + SCTAG_DOM_MUTABLEFILE == 0xffff8004 && + SCTAG_DOM_FILE == 0xffff8005 && + SCTAG_DOM_WASM == 0xffff8006, + "You changed our structured clone tag values and just ate " + "everyone's IndexedDB data. I hope you are happy."); + + if (aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE || + aTag == SCTAG_DOM_BLOB || + aTag == SCTAG_DOM_FILE || + aTag == SCTAG_DOM_MUTABLEFILE || + aTag == SCTAG_DOM_WASM) { + auto* cloneReadInfo = static_cast<StructuredCloneReadInfo*>(aClosure); + + JS::Rooted<JSObject*> result(aCx); + + if (aTag == SCTAG_DOM_WASM) { + WasmModuleData data(aData); + if (NS_WARN_IF(!ReadWasmModule(aReader, &data))) { + return nullptr; + } + + MOZ_ASSERT(data.compiledIndex == data.bytecodeIndex + 1); + MOZ_ASSERT(!data.flags); + + if (data.bytecodeIndex >= cloneReadInfo->mFiles.Length() || + data.compiledIndex >= cloneReadInfo->mFiles.Length()) { + MOZ_ASSERT(false, "Bad index value!"); + return nullptr; + } + + StructuredCloneFile& file = cloneReadInfo->mFiles[data.compiledIndex]; + + if (NS_WARN_IF(!Traits::CreateAndWrapWasmModule(aCx, + file, + data, + &result))) { + return nullptr; + } + + return result; + } + + if (aData >= cloneReadInfo->mFiles.Length()) { + MOZ_ASSERT(false, "Bad index value!"); + return nullptr; + } + + StructuredCloneFile& file = cloneReadInfo->mFiles[aData]; + + if (aTag == SCTAG_DOM_MUTABLEFILE) { + MutableFileData data; + if (NS_WARN_IF(!ReadFileHandle(aReader, &data))) { + return nullptr; + } + + if (NS_WARN_IF(!Traits::CreateAndWrapMutableFile(aCx, + file, + data, + &result))) { + return nullptr; + } + + return result; + } + + BlobOrFileData data; + if (NS_WARN_IF(!ReadBlobOrFile(aReader, aTag, &data))) { + return nullptr; + } + + if (NS_WARN_IF(!Traits::CreateAndWrapBlobOrFile(aCx, + cloneReadInfo->mDatabase, + file, + data, + &result))) { + return nullptr; + } + + return result; + } + + return StructuredCloneHolder::ReadFullySerializableObjects(aCx, aReader, + aTag); +} + +} // namespace + +const JSClass IDBObjectStore::sDummyPropJSClass = { + "IDBObjectStore Dummy", + 0 /* flags */ +}; + +IDBObjectStore::IDBObjectStore(IDBTransaction* aTransaction, + const ObjectStoreSpec* aSpec) + : mTransaction(aTransaction) + , mCachedKeyPath(JS::UndefinedValue()) + , mSpec(aSpec) + , mId(aSpec->metadata().id()) + , mRooted(false) +{ + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + MOZ_ASSERT(aSpec); +} + +IDBObjectStore::~IDBObjectStore() +{ + AssertIsOnOwningThread(); + + if (mRooted) { + mCachedKeyPath.setUndefined(); + mozilla::DropJSObjects(this); + } +} + +// static +already_AddRefed<IDBObjectStore> +IDBObjectStore::Create(IDBTransaction* aTransaction, + const ObjectStoreSpec& aSpec) +{ + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + + RefPtr<IDBObjectStore> objectStore = + new IDBObjectStore(aTransaction, &aSpec); + + return objectStore.forget(); +} + +// static +nsresult +IDBObjectStore::AppendIndexUpdateInfo( + int64_t aIndexID, + const KeyPath& aKeyPath, + bool aUnique, + bool aMultiEntry, + const nsCString& aLocale, + JSContext* aCx, + JS::Handle<JS::Value> aVal, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray) +{ + nsresult rv; + +#ifdef ENABLE_INTL_API + const bool localeAware = !aLocale.IsEmpty(); +#endif + + if (!aMultiEntry) { + Key key; + rv = aKeyPath.ExtractKey(aCx, aVal, key); + + // If an index's keyPath doesn't match an object, we ignore that object. + if (rv == NS_ERROR_DOM_INDEXEDDB_DATA_ERR || key.IsUnset()) { + return NS_OK; + } + + if (NS_FAILED(rv)) { + return rv; + } + + IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement(); + updateInfo->indexId() = aIndexID; + updateInfo->value() = key; +#ifdef ENABLE_INTL_API + if (localeAware) { + rv = key.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } +#endif + + return NS_OK; + } + + JS::Rooted<JS::Value> val(aCx); + if (NS_FAILED(aKeyPath.ExtractKeyAsJSVal(aCx, aVal, val.address()))) { + return NS_OK; + } + + bool isArray; + if (!JS_IsArrayObject(aCx, val, &isArray)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + if (isArray) { + JS::Rooted<JSObject*> array(aCx, &val.toObject()); + uint32_t arrayLength; + if (NS_WARN_IF(!JS_GetArrayLength(aCx, array, &arrayLength))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { + JS::Rooted<JS::Value> arrayItem(aCx); + if (NS_WARN_IF(!JS_GetElement(aCx, array, arrayIndex, &arrayItem))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + Key value; + if (NS_FAILED(value.SetFromJSVal(aCx, arrayItem)) || + value.IsUnset()) { + // Not a value we can do anything with, ignore it. + continue; + } + + IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement(); + updateInfo->indexId() = aIndexID; + updateInfo->value() = value; +#ifdef ENABLE_INTL_API + if (localeAware) { + rv = value.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } +#endif + } + } + else { + Key value; + if (NS_FAILED(value.SetFromJSVal(aCx, val)) || + value.IsUnset()) { + // Not a value we can do anything with, ignore it. + return NS_OK; + } + + IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement(); + updateInfo->indexId() = aIndexID; + updateInfo->value() = value; +#ifdef ENABLE_INTL_API + if (localeAware) { + rv = value.ToLocaleBasedKey(updateInfo->localizedValue(), aLocale); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } +#endif + } + + return NS_OK; +} + +// static +void +IDBObjectStore::ClearCloneReadInfo(StructuredCloneReadInfo& aReadInfo) +{ + // This is kind of tricky, we only want to release stuff on the main thread, + // but we can end up being called on other threads if we have already been + // cleared on the main thread. + if (!aReadInfo.mFiles.Length()) { + return; + } + + aReadInfo.mFiles.Clear(); +} + +// static +bool +IDBObjectStore::DeserializeValue(JSContext* aCx, + StructuredCloneReadInfo& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue) +{ + MOZ_ASSERT(aCx); + + if (!aCloneReadInfo.mData.Size()) { + aValue.setUndefined(); + return true; + } + + MOZ_ASSERT(!(aCloneReadInfo.mData.Size() % sizeof(uint64_t))); + + JSAutoRequest ar(aCx); + + static const JSStructuredCloneCallbacks callbacks = { + CommonStructuredCloneReadCallback<ValueDeserializationHelper>, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr + }; + + // FIXME: Consider to use StructuredCloneHolder here and in other + // deserializing methods. + if (!JS_ReadStructuredClone(aCx, aCloneReadInfo.mData, JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::SameProcessSameThread, + aValue, &callbacks, &aCloneReadInfo)) { + return false; + } + + return true; +} + +// static +bool +IDBObjectStore::DeserializeIndexValue(JSContext* aCx, + StructuredCloneReadInfo& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCx); + + if (!aCloneReadInfo.mData.Size()) { + aValue.setUndefined(); + return true; + } + + MOZ_ASSERT(!(aCloneReadInfo.mData.Size() % sizeof(uint64_t))); + + JSAutoRequest ar(aCx); + + static const JSStructuredCloneCallbacks callbacks = { + CommonStructuredCloneReadCallback<IndexDeserializationHelper>, + nullptr, + nullptr + }; + + if (!JS_ReadStructuredClone(aCx, aCloneReadInfo.mData, JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::SameProcessSameThread, + aValue, &callbacks, &aCloneReadInfo)) { + return false; + } + + return true; +} + +#if !defined(MOZ_B2G) + +// static +bool +IDBObjectStore::DeserializeUpgradeValue(JSContext* aCx, + StructuredCloneReadInfo& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCx); + + if (!aCloneReadInfo.mData.Size()) { + aValue.setUndefined(); + return true; + } + + MOZ_ASSERT(!(aCloneReadInfo.mData.Size() % sizeof(uint64_t))); + + JSAutoRequest ar(aCx); + + static JSStructuredCloneCallbacks callbacks = { + CommonStructuredCloneReadCallback<UpgradeDeserializationHelper>, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr + }; + + if (!JS_ReadStructuredClone(aCx, aCloneReadInfo.mData, JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::SameProcessSameThread, + aValue, &callbacks, &aCloneReadInfo)) { + return false; + } + + return true; +} + +#endif // MOZ_B2G + +#ifdef DEBUG + +void +IDBObjectStore::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +nsresult +IDBObjectStore::GetAddInfo(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKeyVal, + StructuredCloneWriteInfo& aCloneWriteInfo, + Key& aKey, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray) +{ + // Return DATA_ERR if a key was passed in and this objectStore uses inline + // keys. + if (!aKeyVal.isUndefined() && HasValidKeyPath()) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + bool isAutoIncrement = AutoIncrement(); + + nsresult rv; + + if (!HasValidKeyPath()) { + // Out-of-line keys must be passed in. + rv = aKey.SetFromJSVal(aCx, aKeyVal); + if (NS_FAILED(rv)) { + return rv; + } + } else if (!isAutoIncrement) { + rv = GetKeyPath().ExtractKey(aCx, aValue, aKey); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Return DATA_ERR if no key was specified this isn't an autoIncrement + // objectStore. + if (aKey.IsUnset() && !isAutoIncrement) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + // Figure out indexes and the index values to update here. + const nsTArray<IndexMetadata>& indexes = mSpec->indexes(); + + const uint32_t idxCount = indexes.Length(); + aUpdateInfoArray.SetCapacity(idxCount); // Pretty good estimate + + for (uint32_t idxIndex = 0; idxIndex < idxCount; idxIndex++) { + const IndexMetadata& metadata = indexes[idxIndex]; + + rv = AppendIndexUpdateInfo(metadata.id(), metadata.keyPath(), + metadata.unique(), metadata.multiEntry(), + metadata.locale(), aCx, aValue, + aUpdateInfoArray); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + GetAddInfoClosure data(aCloneWriteInfo, aValue); + + if (isAutoIncrement && HasValidKeyPath()) { + MOZ_ASSERT(aKey.IsUnset()); + + rv = GetKeyPath().ExtractOrCreateKey(aCx, + aValue, + aKey, + &GetAddInfoCallback, + &data); + } else { + rv = GetAddInfoCallback(aCx, &data); + } + + return rv; +} + +already_AddRefed<IDBRequest> +IDBObjectStore::AddOrPut(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + bool aOverwrite, + bool aFromCursor, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCx); + MOZ_ASSERT_IF(aFromCursor, aOverwrite); + + if (mTransaction->GetMode() == IDBTransaction::CLEANUP || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + if (!mTransaction->IsWriteAllowed()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); + return nullptr; + } + + JS::Rooted<JS::Value> value(aCx, aValue); + Key key; + StructuredCloneWriteInfo cloneWriteInfo(mTransaction->Database()); + nsTArray<IndexUpdateInfo> updateInfo; + + aRv = GetAddInfo(aCx, value, aKey, cloneWriteInfo, key, updateInfo); + if (aRv.Failed()) { + return nullptr; + } + + // Check the size limit of the serialized message which mainly consists of + // a StructuredCloneBuffer, an encoded object key, and the encoded index keys. + // kMaxIDBMsgOverhead covers the minor stuff not included in this calculation + // because the precise calculation would slow down this AddOrPut operation. + static const size_t kMaxIDBMsgOverhead = 1024 * 1024; // 1MB + const uint32_t maximalSizeFromPref = + IndexedDatabaseManager::MaxSerializedMsgSize(); + MOZ_ASSERT(maximalSizeFromPref > kMaxIDBMsgOverhead); + const size_t kMaxMessageSize = maximalSizeFromPref - kMaxIDBMsgOverhead; + + size_t indexUpdateInfoSize = 0; + for (size_t i = 0; i < updateInfo.Length(); i++) { + indexUpdateInfoSize += updateInfo[i].value().GetBuffer().Length(); + indexUpdateInfoSize += updateInfo[i].localizedValue().GetBuffer().Length(); + } + + size_t messageSize = cloneWriteInfo.mCloneBuffer.data().Size() + + key.GetBuffer().Length() + indexUpdateInfoSize; + + if (messageSize > kMaxMessageSize) { + IDB_REPORT_INTERNAL_ERR(); + aRv.ThrowDOMException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + nsPrintfCString("The serialized value is too large" + " (size=%zu bytes, max=%zu bytes).", + messageSize, kMaxMessageSize)); + return nullptr; + } + + ObjectStoreAddPutParams commonParams; + commonParams.objectStoreId() = Id(); + commonParams.cloneInfo().data().data = Move(cloneWriteInfo.mCloneBuffer.data()); + commonParams.cloneInfo().offsetToKeyProp() = cloneWriteInfo.mOffsetToKeyProp; + commonParams.key() = key; + commonParams.indexUpdateInfos().SwapElements(updateInfo); + + // Convert any blobs or mutable files into FileAddInfo. + nsTArray<StructuredCloneFile>& files = cloneWriteInfo.mFiles; + + if (!files.IsEmpty()) { + const uint32_t count = files.Length(); + + FallibleTArray<FileAddInfo> fileAddInfos; + if (NS_WARN_IF(!fileAddInfos.SetCapacity(count, fallible))) { + aRv = NS_ERROR_OUT_OF_MEMORY; + return nullptr; + } + + IDBDatabase* database = mTransaction->Database(); + + for (uint32_t index = 0; index < count; index++) { + StructuredCloneFile& file = files[index]; + + FileAddInfo* fileAddInfo = fileAddInfos.AppendElement(fallible); + MOZ_ASSERT(fileAddInfo); + + switch (file.mType) { + case StructuredCloneFile::eBlob: { + MOZ_ASSERT(file.mBlob); + MOZ_ASSERT(!file.mMutableFile); + + PBackgroundIDBDatabaseFileChild* fileActor = + database->GetOrCreateFileActorForBlob(file.mBlob); + if (NS_WARN_IF(!fileActor)) { + IDB_REPORT_INTERNAL_ERR(); + aRv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + return nullptr; + } + + fileAddInfo->file() = fileActor; + fileAddInfo->type() = StructuredCloneFile::eBlob; + + break; + } + + case StructuredCloneFile::eMutableFile: { + MOZ_ASSERT(file.mMutableFile); + MOZ_ASSERT(!file.mBlob); + + PBackgroundMutableFileChild* mutableFileActor = + file.mMutableFile->GetBackgroundActor(); + if (NS_WARN_IF(!mutableFileActor)) { + IDB_REPORT_INTERNAL_ERR(); + aRv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + return nullptr; + } + + fileAddInfo->file() = mutableFileActor; + fileAddInfo->type() = StructuredCloneFile::eMutableFile; + + break; + } + + case StructuredCloneFile::eWasmBytecode: + case StructuredCloneFile::eWasmCompiled: { + MOZ_ASSERT(file.mBlob); + MOZ_ASSERT(!file.mMutableFile); + + PBackgroundIDBDatabaseFileChild* fileActor = + database->GetOrCreateFileActorForBlob(file.mBlob); + if (NS_WARN_IF(!fileActor)) { + IDB_REPORT_INTERNAL_ERR(); + aRv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + return nullptr; + } + + fileAddInfo->file() = fileActor; + fileAddInfo->type() = file.mType; + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + } + + commonParams.fileAddInfos().SwapElements(fileAddInfos); + } + + RequestParams params; + if (aOverwrite) { + params = ObjectStorePutParams(commonParams); + } else { + params = ObjectStoreAddParams(commonParams); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (!aFromCursor) { + if (aOverwrite) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).put(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.put()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(key)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).add(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.add()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(key)); + } + } + + mTransaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBObjectStore::GetAllInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + const int64_t id = Id(); + + OptionalKeyRange optionalKeyRange; + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + optionalKeyRange = serializedKeyRange; + } else { + optionalKeyRange = void_t(); + } + + const uint32_t limit = aLimit.WasPassed() ? aLimit.Value() : 0; + + RequestParams params; + if (aKeysOnly) { + params = ObjectStoreGetAllKeysParams(id, optionalKeyRange, limit); + } else { + params = ObjectStoreGetAllParams(id, optionalKeyRange, limit); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (aKeysOnly) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "getAllKeys(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.getAllKeys()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(aLimit)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "getAll(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.getAll()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(aLimit)); + } + + mTransaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBObjectStore::Clear(JSContext* aCx, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + if (!mTransaction->IsWriteAllowed()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); + return nullptr; + } + + ObjectStoreClearParams params; + params.objectStoreId() = Id(); + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).clear()", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.clear()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this)); + + mTransaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBIndex> +IDBObjectStore::Index(const nsAString& aName, ErrorResult &aRv) +{ + AssertIsOnOwningThread(); + + if (mTransaction->IsCommittingOrDone() || mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + const nsTArray<IndexMetadata>& indexes = mSpec->indexes(); + + const IndexMetadata* metadata = nullptr; + + for (uint32_t idxCount = indexes.Length(), idxIndex = 0; + idxIndex < idxCount; + idxIndex++) { + const IndexMetadata& index = indexes[idxIndex]; + if (index.name() == aName) { + metadata = &index; + break; + } + } + + if (!metadata) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return nullptr; + } + + const int64_t desiredId = metadata->id(); + + RefPtr<IDBIndex> index; + + for (uint32_t idxCount = mIndexes.Length(), idxIndex = 0; + idxIndex < idxCount; + idxIndex++) { + RefPtr<IDBIndex>& existingIndex = mIndexes[idxIndex]; + + if (existingIndex->Id() == desiredId) { + index = existingIndex; + break; + } + } + + if (!index) { + index = IDBIndex::Create(this, *metadata); + MOZ_ASSERT(index); + + mIndexes.AppendElement(index); + } + + return index.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBObjectStore) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBObjectStore) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedKeyPath) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBObjectStore) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIndexes) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeletedIndexes) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBObjectStore) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + + // Don't unlink mTransaction! + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIndexes) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDeletedIndexes) + + tmp->mCachedKeyPath.setUndefined(); + + if (tmp->mRooted) { + mozilla::DropJSObjects(tmp); + tmp->mRooted = false; + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBObjectStore) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBObjectStore) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBObjectStore) + +JSObject* +IDBObjectStore::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBObjectStoreBinding::Wrap(aCx, this, aGivenProto); +} + +nsPIDOMWindowInner* +IDBObjectStore::GetParentObject() const +{ + return mTransaction->GetParentObject(); +} + +void +IDBObjectStore::GetKeyPath(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) +{ + if (!mCachedKeyPath.isUndefined()) { + aResult.set(mCachedKeyPath); + return; + } + + aRv = GetKeyPath().ToJSVal(aCx, mCachedKeyPath); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (mCachedKeyPath.isGCThing()) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + aResult.set(mCachedKeyPath); +} + +already_AddRefed<DOMStringList> +IDBObjectStore::IndexNames() +{ + AssertIsOnOwningThread(); + + const nsTArray<IndexMetadata>& indexes = mSpec->indexes(); + + RefPtr<DOMStringList> list = new DOMStringList(); + + if (!indexes.IsEmpty()) { + nsTArray<nsString>& listNames = list->StringArray(); + listNames.SetCapacity(indexes.Length()); + + for (uint32_t index = 0; index < indexes.Length(); index++) { + listNames.InsertElementSorted(indexes[index].name()); + } + } + + return list.forget(); +} + +already_AddRefed<IDBRequest> +IDBObjectStore::GetInternal(bool aKeyOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (aRv.Failed()) { + return nullptr; + } + + if (!keyRange) { + // Must specify a key or keyRange for get(). + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + const int64_t id = Id(); + + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + RequestParams params; + if (aKeyOnly) { + params = ObjectStoreGetKeyParams(id, serializedKeyRange); + } else { + params = ObjectStoreGetParams(id, serializedKeyRange); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).get(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.get()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + + mTransaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBObjectStore::DeleteInternal(JSContext* aCx, + JS::Handle<JS::Value> aKey, + bool aFromCursor, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + if (!mTransaction->IsWriteAllowed()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_READ_ONLY_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (NS_WARN_IF((aRv.Failed()))) { + return nullptr; + } + + if (!keyRange) { + // Must specify a key or keyRange for delete(). + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + ObjectStoreDeleteParams params; + params.objectStoreId() = Id(); + keyRange->ToSerialized(params.keyRange()); + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (!aFromCursor) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).delete(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.delete()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + } + + mTransaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBIndex> +IDBObjectStore::CreateIndex(const nsAString& aName, + const StringOrStringSequence& aKeyPath, + const IDBIndexParameters& aOptionalParameters, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mTransaction->GetMode() != IDBTransaction::VERSION_CHANGE || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + IDBTransaction* transaction = IDBTransaction::GetCurrent(); + if (!transaction || transaction != mTransaction || !transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + auto& indexes = const_cast<nsTArray<IndexMetadata>&>(mSpec->indexes()); + for (uint32_t count = indexes.Length(), index = 0; + index < count; + index++) { + if (aName == indexes[index].name()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR); + return nullptr; + } + } + + KeyPath keyPath(0); + if (aKeyPath.IsString()) { + if (NS_FAILED(KeyPath::Parse(aKeyPath.GetAsString(), &keyPath)) || + !keyPath.IsValid()) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + } else { + MOZ_ASSERT(aKeyPath.IsStringSequence()); + if (aKeyPath.GetAsStringSequence().IsEmpty() || + NS_FAILED(KeyPath::Parse(aKeyPath.GetAsStringSequence(), &keyPath)) || + !keyPath.IsValid()) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + } + + if (aOptionalParameters.mMultiEntry && keyPath.IsArray()) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + +#ifdef DEBUG + for (uint32_t count = mIndexes.Length(), index = 0; + index < count; + index++) { + MOZ_ASSERT(mIndexes[index]->Name() != aName); + } +#endif + + const IndexMetadata* oldMetadataElements = + indexes.IsEmpty() ? nullptr : indexes.Elements(); + + // With this setup we only validate the passed in locale name by the time we + // get to encoding Keys. Maybe we should do it here right away and error out. + + // Valid locale names are always ASCII as per BCP-47. + nsCString locale = NS_LossyConvertUTF16toASCII(aOptionalParameters.mLocale); + bool autoLocale = locale.EqualsASCII("auto"); +#ifdef ENABLE_INTL_API + if (autoLocale) { + locale = IndexedDatabaseManager::GetLocale(); + } +#endif + + IndexMetadata* metadata = indexes.AppendElement( + IndexMetadata(transaction->NextIndexId(), nsString(aName), keyPath, + locale, + aOptionalParameters.mUnique, + aOptionalParameters.mMultiEntry, + autoLocale)); + + if (oldMetadataElements && + oldMetadataElements != indexes.Elements()) { + MOZ_ASSERT(indexes.Length() > 1); + + // Array got moved, update the spec pointers for all live indexes. + RefreshSpec(/* aMayDelete */ false); + } + + transaction->CreateIndex(this, *metadata); + + RefPtr<IDBIndex> index = IDBIndex::Create(this, *metadata); + MOZ_ASSERT(index); + + mIndexes.AppendElement(index); + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).createIndex(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.createIndex()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(index)); + + return index.forget(); +} + +void +IDBObjectStore::DeleteIndex(const nsAString& aName, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mTransaction->GetMode() != IDBTransaction::VERSION_CHANGE || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + IDBTransaction* transaction = IDBTransaction::GetCurrent(); + if (!transaction || transaction != mTransaction || !transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + auto& metadataArray = const_cast<nsTArray<IndexMetadata>&>(mSpec->indexes()); + + int64_t foundId = 0; + + for (uint32_t metadataCount = metadataArray.Length(), metadataIndex = 0; + metadataIndex < metadataCount; + metadataIndex++) { + const IndexMetadata& metadata = metadataArray[metadataIndex]; + MOZ_ASSERT(metadata.id()); + + if (aName == metadata.name()) { + foundId = metadata.id(); + + // Must do this before altering the metadata array! + for (uint32_t indexCount = mIndexes.Length(), indexIndex = 0; + indexIndex < indexCount; + indexIndex++) { + RefPtr<IDBIndex>& index = mIndexes[indexIndex]; + + if (index->Id() == foundId) { + index->NoteDeletion(); + + RefPtr<IDBIndex>* deletedIndex = + mDeletedIndexes.AppendElement(); + deletedIndex->swap(mIndexes[indexIndex]); + + mIndexes.RemoveElementAt(indexIndex); + break; + } + } + + metadataArray.RemoveElementAt(metadataIndex); + + RefreshSpec(/* aMayDelete */ false); + break; + } + } + + if (!foundId) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return; + } + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "deleteIndex(\"%s\")", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.deleteIndex()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + NS_ConvertUTF16toUTF8(aName).get()); + + transaction->DeleteIndex(this, foundId); +} + +already_AddRefed<IDBRequest> +IDBObjectStore::Count(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aKey, getter_AddRefs(keyRange)); + if (aRv.Failed()) { + return nullptr; + } + + ObjectStoreCountParams params; + params.objectStoreId() = Id(); + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + params.optionalKeyRange() = serializedKeyRange; + } else { + params.optionalKeyRange() = void_t(); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).count(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.count()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + + mTransaction->StartRequest(request, params); + + return request.forget(); +} + +already_AddRefed<IDBRequest> +IDBObjectStore::OpenCursorInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCx); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + aRv = IDBKeyRange::FromJSVal(aCx, aRange, getter_AddRefs(keyRange)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + int64_t objectStoreId = Id(); + + OptionalKeyRange optionalKeyRange; + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + optionalKeyRange = Move(serializedKeyRange); + } else { + optionalKeyRange = void_t(); + } + + IDBCursor::Direction direction = IDBCursor::ConvertDirection(aDirection); + + OpenCursorParams params; + if (aKeysOnly) { + ObjectStoreOpenKeyCursorParams openParams; + openParams.objectStoreId() = objectStoreId; + openParams.optionalKeyRange() = Move(optionalKeyRange); + openParams.direction() = direction; + + params = Move(openParams); + } else { + ObjectStoreOpenCursorParams openParams; + openParams.objectStoreId() = objectStoreId; + openParams.optionalKeyRange() = Move(optionalKeyRange); + openParams.direction() = direction; + + params = Move(openParams); + } + + RefPtr<IDBRequest> request = GenerateRequest(aCx, this); + MOZ_ASSERT(request); + + if (aKeysOnly) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "openKeyCursor(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: " + "IDBObjectStore.openKeyCursor()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(direction)); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s)." + "openCursor(%s, %s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.openCursor()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), + IDB_LOG_STRINGIFY(direction)); + } + + BackgroundCursorChild* actor = + new BackgroundCursorChild(request, this, direction); + + mTransaction->OpenCursor(actor, params); + + return request.forget(); +} + +void +IDBObjectStore::RefreshSpec(bool aMayDelete) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mDeletedSpec, mSpec == mDeletedSpec); + + const DatabaseSpec* dbSpec = mTransaction->Database()->Spec(); + MOZ_ASSERT(dbSpec); + + const nsTArray<ObjectStoreSpec>& objectStores = dbSpec->objectStores(); + + bool found = false; + + for (uint32_t objCount = objectStores.Length(), objIndex = 0; + objIndex < objCount; + objIndex++) { + const ObjectStoreSpec& objSpec = objectStores[objIndex]; + + if (objSpec.metadata().id() == Id()) { + mSpec = &objSpec; + + for (uint32_t idxCount = mIndexes.Length(), idxIndex = 0; + idxIndex < idxCount; + idxIndex++) { + mIndexes[idxIndex]->RefreshMetadata(aMayDelete); + } + + for (uint32_t idxCount = mDeletedIndexes.Length(), idxIndex = 0; + idxIndex < idxCount; + idxIndex++) { + mDeletedIndexes[idxIndex]->RefreshMetadata(false); + } + + found = true; + break; + } + } + + MOZ_ASSERT_IF(!aMayDelete && !mDeletedSpec, found); + + if (found) { + MOZ_ASSERT(mSpec != mDeletedSpec); + mDeletedSpec = nullptr; + } else { + NoteDeletion(); + } +} + +const ObjectStoreSpec& +IDBObjectStore::Spec() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return *mSpec; +} + +void +IDBObjectStore::NoteDeletion() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + MOZ_ASSERT(Id() == mSpec->metadata().id()); + + if (mDeletedSpec) { + MOZ_ASSERT(mDeletedSpec == mSpec); + return; + } + + // Copy the spec here. + mDeletedSpec = new ObjectStoreSpec(*mSpec); + mDeletedSpec->indexes().Clear(); + + mSpec = mDeletedSpec; + + if (!mIndexes.IsEmpty()) { + for (uint32_t count = mIndexes.Length(), index = 0; + index < count; + index++) { + mIndexes[index]->NoteDeletion(); + } + } +} + +const nsString& +IDBObjectStore::Name() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return mSpec->metadata().name(); +} + +void +IDBObjectStore::SetName(const nsAString& aName, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (mTransaction->GetMode() != IDBTransaction::VERSION_CHANGE || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + IDBTransaction* transaction = IDBTransaction::GetCurrent(); + if (!transaction || transaction != mTransaction || !transaction->IsOpen()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + if (aName == mSpec->metadata().name()) { + return; + } + + // Cache logging string of this object store before renaming. + const LoggingString loggingOldObjectStore(this); + + nsresult rv = + transaction->Database()->RenameObjectStore(mSpec->metadata().id(), + aName); + + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "database(%s).transaction(%s).objectStore(%s).rename(%s)", + "IndexedDB %s: C T[%lld] R[%llu]: IDBObjectStore.rename()", + IDB_LOG_ID_STRING(), + mTransaction->LoggingSerialNumber(), + requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(mTransaction), + loggingOldObjectStore.get(), + IDB_LOG_STRINGIFY(this)); + + transaction->RenameObjectStore(mSpec->metadata().id(), aName); +} + +bool +IDBObjectStore::AutoIncrement() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return mSpec->metadata().autoIncrement(); +} + +const indexedDB::KeyPath& +IDBObjectStore::GetKeyPath() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return mSpec->metadata().keyPath(); +} + +bool +IDBObjectStore::HasValidKeyPath() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return GetKeyPath().IsValid(); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBObjectStore.h b/dom/indexedDB/IDBObjectStore.h new file mode 100644 index 000000000..7a255a4af --- /dev/null +++ b/dom/indexedDB/IDBObjectStore.h @@ -0,0 +1,378 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbobjectstore_h__ +#define mozilla_dom_idbobjectstore_h__ + +#include "js/RootingAPI.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IDBIndexBinding.h" +#include "nsAutoPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +struct JSClass; +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class DOMStringList; +class IDBCursor; +class IDBRequest; +class IDBTransaction; +class StringOrStringSequence; +template <typename> class Sequence; + +namespace indexedDB { +class Key; +class KeyPath; +class IndexUpdateInfo; +class ObjectStoreSpec; +struct StructuredCloneReadInfo; +} // namespace indexedDB + +class IDBObjectStore final + : public nsISupports + , public nsWrapperCache +{ + typedef indexedDB::IndexUpdateInfo IndexUpdateInfo; + typedef indexedDB::Key Key; + typedef indexedDB::KeyPath KeyPath; + typedef indexedDB::ObjectStoreSpec ObjectStoreSpec; + typedef indexedDB::StructuredCloneReadInfo StructuredCloneReadInfo; + + // For AddOrPut() and DeleteInternal(). + friend class IDBCursor; + + static const JSClass sDummyPropJSClass; + + RefPtr<IDBTransaction> mTransaction; + JS::Heap<JS::Value> mCachedKeyPath; + + // This normally points to the ObjectStoreSpec owned by the parent IDBDatabase + // object. However, if this objectStore is part of a versionchange transaction + // and it gets deleted then the spec is copied into mDeletedSpec and mSpec is + // set to point at mDeletedSpec. + const ObjectStoreSpec* mSpec; + nsAutoPtr<ObjectStoreSpec> mDeletedSpec; + + nsTArray<RefPtr<IDBIndex>> mIndexes; + nsTArray<RefPtr<IDBIndex>> mDeletedIndexes; + + const int64_t mId; + bool mRooted; + +public: + struct StructuredCloneWriteInfo; + + static already_AddRefed<IDBObjectStore> + Create(IDBTransaction* aTransaction, const ObjectStoreSpec& aSpec); + + static nsresult + AppendIndexUpdateInfo(int64_t aIndexID, + const KeyPath& aKeyPath, + bool aUnique, + bool aMultiEntry, + const nsCString& aLocale, + JSContext* aCx, + JS::Handle<JS::Value> aObject, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray); + + static void + ClearCloneReadInfo(StructuredCloneReadInfo& aReadInfo); + + static bool + DeserializeValue(JSContext* aCx, + StructuredCloneReadInfo& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue); + + static bool + DeserializeIndexValue(JSContext* aCx, + StructuredCloneReadInfo& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue); + +#if !defined(MOZ_B2G) + static bool + DeserializeUpgradeValue(JSContext* aCx, + StructuredCloneReadInfo& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue); +#endif + + static const JSClass* + DummyPropClass() + { + return &sDummyPropJSClass; + } + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + int64_t + Id() const + { + AssertIsOnOwningThread(); + + return mId; + } + + const nsString& + Name() const; + + bool + AutoIncrement() const; + + const KeyPath& + GetKeyPath() const; + + bool + HasValidKeyPath() const; + + nsPIDOMWindowInner* + GetParentObject() const; + + void + GetName(nsString& aName) const + { + AssertIsOnOwningThread(); + + aName = Name(); + } + + void + SetName(const nsAString& aName, ErrorResult& aRv); + + void + GetKeyPath(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv); + + already_AddRefed<DOMStringList> + IndexNames(); + + IDBTransaction* + Transaction() const + { + AssertIsOnOwningThread(); + + return mTransaction; + } + + already_AddRefed<IDBRequest> + Add(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return AddOrPut(aCx, aValue, aKey, false, /* aFromCursor */ false, aRv); + } + + already_AddRefed<IDBRequest> + Put(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return AddOrPut(aCx, aValue, aKey, true, /* aFromCursor */ false, aRv); + } + + already_AddRefed<IDBRequest> + Delete(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return DeleteInternal(aCx, aKey, /* aFromCursor */ false, aRv); + } + + already_AddRefed<IDBRequest> + Get(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ false, aCx, aKey, aRv); + } + + already_AddRefed<IDBRequest> + GetKey(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ true, aCx, aKey, aRv); + } + + already_AddRefed<IDBRequest> + Clear(JSContext* aCx, ErrorResult& aRv); + + already_AddRefed<IDBIndex> + CreateIndex(const nsAString& aName, + const StringOrStringSequence& aKeyPath, + const IDBIndexParameters& aOptionalParameters, + ErrorResult& aRv); + + already_AddRefed<IDBIndex> + Index(const nsAString& aName, ErrorResult &aRv); + + void + DeleteIndex(const nsAString& aIndexName, ErrorResult& aRv); + + already_AddRefed<IDBRequest> + Count(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + GetAll(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ false, aCx, aKey, aLimit, aRv); + } + + already_AddRefed<IDBRequest> + GetAllKeys(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ true, aCx, aKey, aLimit, aRv); + } + + already_AddRefed<IDBRequest> + OpenCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ false, aCx, aRange, aDirection, + aRv); + } + + already_AddRefed<IDBRequest> + OpenCursor(JSContext* aCx, + IDBCursorDirection aDirection, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ false, aCx, + JS::UndefinedHandleValue, aDirection, aRv); + } + + already_AddRefed<IDBRequest> + OpenKeyCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) + { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ true, aCx, aRange, aDirection, + aRv); + } + + void + RefreshSpec(bool aMayDelete); + + const ObjectStoreSpec& + Spec() const; + + void + NoteDeletion(); + + bool + IsDeleted() const + { + AssertIsOnOwningThread(); + + return !!mDeletedSpec; + } + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBObjectStore) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBObjectStore(IDBTransaction* aTransaction, const ObjectStoreSpec* aSpec); + + ~IDBObjectStore(); + + nsresult + GetAddInfo(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKeyVal, + StructuredCloneWriteInfo& aCloneWriteInfo, + Key& aKey, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray); + + already_AddRefed<IDBRequest> + AddOrPut(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + bool aOverwrite, + bool aFromCursor, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + DeleteInternal(JSContext* aCx, + JS::Handle<JS::Value> aKey, + bool aFromCursor, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + GetInternal(bool aKeyOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + GetAllInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv); + + already_AddRefed<IDBRequest> + OpenCursorInternal(bool aKeysOnly, + JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbobjectstore_h__ diff --git a/dom/indexedDB/IDBRequest.cpp b/dom/indexedDB/IDBRequest.cpp new file mode 100644 index 000000000..919d3adc7 --- /dev/null +++ b/dom/indexedDB/IDBRequest.cpp @@ -0,0 +1,661 @@ +/* -*- 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 "IDBRequest.h" + +#include "BackgroundChildImpl.h" +#include "IDBCursor.h" +#include "IDBDatabase.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBObjectStore.h" +#include "IDBTransaction.h" +#include "IndexedDatabaseManager.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Move.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/IDBOpenDBRequestBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIScriptContext.h" +#include "nsJSUtils.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" +#include "ReportInternalError.h" +#include "WorkerHolder.h" +#include "WorkerPrivate.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::dom::workers; +using namespace mozilla::ipc; + +namespace { + +NS_DEFINE_IID(kIDBRequestIID, PRIVATE_IDBREQUEST_IID); + +} // namespace + +IDBRequest::IDBRequest(IDBDatabase* aDatabase) + : IDBWrapperCache(aDatabase) +#ifdef DEBUG + , mOwningThread(nullptr) +#endif + , mLoggingSerialNumber(0) + , mLineNo(0) + , mColumn(0) + , mHaveResultOrErrorCode(false) +{ + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + InitMembers(); +} + +IDBRequest::IDBRequest(nsPIDOMWindowInner* aOwner) + : IDBWrapperCache(aOwner) +#ifdef DEBUG + , mOwningThread(nullptr) +#endif + , mLoggingSerialNumber(0) + , mLineNo(0) + , mColumn(0) + , mHaveResultOrErrorCode(false) +{ + InitMembers(); +} + +IDBRequest::~IDBRequest() +{ + AssertIsOnOwningThread(); +} + +#ifdef DEBUG + +void +IDBRequest::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mOwningThread); + MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread); +} + +#endif // DEBUG + +void +IDBRequest::InitMembers() +{ +#ifdef DEBUG + mOwningThread = PR_GetCurrentThread(); +#endif + AssertIsOnOwningThread(); + + mResultVal.setUndefined(); + mLoggingSerialNumber = NextSerialNumber(); + mErrorCode = NS_OK; + mLineNo = 0; + mColumn = 0; + mHaveResultOrErrorCode = false; +} + +// static +already_AddRefed<IDBRequest> +IDBRequest::Create(JSContext* aCx, + IDBDatabase* aDatabase, + IDBTransaction* aTransaction) +{ + MOZ_ASSERT(aCx); + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + RefPtr<IDBRequest> request = new IDBRequest(aDatabase); + CaptureCaller(aCx, request->mFilename, &request->mLineNo, &request->mColumn); + + request->mTransaction = aTransaction; + request->SetScriptOwner(aDatabase->GetScriptOwner()); + + return request.forget(); +} + +// static +already_AddRefed<IDBRequest> +IDBRequest::Create(JSContext* aCx, + IDBObjectStore* aSourceAsObjectStore, + IDBDatabase* aDatabase, + IDBTransaction* aTransaction) +{ + MOZ_ASSERT(aSourceAsObjectStore); + aSourceAsObjectStore->AssertIsOnOwningThread(); + + RefPtr<IDBRequest> request = Create(aCx, aDatabase, aTransaction); + + request->mSourceAsObjectStore = aSourceAsObjectStore; + + return request.forget(); +} + +// static +already_AddRefed<IDBRequest> +IDBRequest::Create(JSContext* aCx, + IDBIndex* aSourceAsIndex, + IDBDatabase* aDatabase, + IDBTransaction* aTransaction) +{ + MOZ_ASSERT(aSourceAsIndex); + aSourceAsIndex->AssertIsOnOwningThread(); + + RefPtr<IDBRequest> request = Create(aCx, aDatabase, aTransaction); + + request->mSourceAsIndex = aSourceAsIndex; + + return request.forget(); +} + +// static +uint64_t +IDBRequest::NextSerialNumber() +{ + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + ThreadLocal* idbThreadLocal = threadLocal->mIndexedDBThreadLocal; + MOZ_ASSERT(idbThreadLocal); + + return idbThreadLocal->NextRequestSN(); +} + +void +IDBRequest::SetLoggingSerialNumber(uint64_t aLoggingSerialNumber) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aLoggingSerialNumber > mLoggingSerialNumber); + + mLoggingSerialNumber = aLoggingSerialNumber; +} + +void +IDBRequest::CaptureCaller(JSContext* aCx, nsAString& aFilename, + uint32_t* aLineNo, uint32_t* aColumn) +{ + MOZ_ASSERT(aFilename.IsEmpty()); + MOZ_ASSERT(aLineNo); + MOZ_ASSERT(aColumn); + + nsJSUtils::GetCallingLocation(aCx, aFilename, aLineNo, aColumn); +} + +void +IDBRequest::GetSource( + Nullable<OwningIDBObjectStoreOrIDBIndexOrIDBCursor>& aSource) const +{ + AssertIsOnOwningThread(); + + MOZ_ASSERT_IF(mSourceAsObjectStore, !mSourceAsIndex); + MOZ_ASSERT_IF(mSourceAsIndex, !mSourceAsObjectStore); + MOZ_ASSERT_IF(mSourceAsCursor, mSourceAsObjectStore || mSourceAsIndex); + + // Always check cursor first since cursor requests hold both the cursor and + // the objectStore or index the cursor came from. + if (mSourceAsCursor) { + aSource.SetValue().SetAsIDBCursor() = mSourceAsCursor; + } else if (mSourceAsObjectStore) { + aSource.SetValue().SetAsIDBObjectStore() = mSourceAsObjectStore; + } else if (mSourceAsIndex) { + aSource.SetValue().SetAsIDBIndex() = mSourceAsIndex; + } else { + aSource.SetNull(); + } +} + +void +IDBRequest::Reset() +{ + AssertIsOnOwningThread(); + + mResultVal.setUndefined(); + mHaveResultOrErrorCode = false; + mError = nullptr; +} + +void +IDBRequest::DispatchNonTransactionError(nsresult aErrorCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_DOM_INDEXEDDB); + + SetError(aErrorCode); + + // Make an error event and fire it at the target. + nsCOMPtr<nsIDOMEvent> event = + CreateGenericEvent(this, + nsDependentString(kErrorEventType), + eDoesBubble, + eCancelable); + MOZ_ASSERT(event); + + bool ignored; + if (NS_FAILED(DispatchEvent(event, &ignored))) { + NS_WARNING("Failed to dispatch event!"); + } +} + +void +IDBRequest::SetError(nsresult aRv) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aRv)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aRv) == NS_ERROR_MODULE_DOM_INDEXEDDB); + MOZ_ASSERT(!mError); + + mHaveResultOrErrorCode = true; + mError = new DOMError(GetOwner(), aRv); + mErrorCode = aRv; + + mResultVal.setUndefined(); +} + +#ifdef DEBUG + +nsresult +IDBRequest::GetErrorCode() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mHaveResultOrErrorCode); + + return mErrorCode; +} + +DOMError* +IDBRequest::GetErrorAfterResult() const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mHaveResultOrErrorCode); + + return mError; +} + +#endif // DEBUG + +void +IDBRequest::GetCallerLocation(nsAString& aFilename, uint32_t* aLineNo, + uint32_t* aColumn) const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aLineNo); + MOZ_ASSERT(aColumn); + + aFilename = mFilename; + *aLineNo = mLineNo; + *aColumn = mColumn; +} + +IDBRequestReadyState +IDBRequest::ReadyState() const +{ + AssertIsOnOwningThread(); + + return IsPending() ? + IDBRequestReadyState::Pending : + IDBRequestReadyState::Done; +} + +void +IDBRequest::SetSource(IDBCursor* aSource) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aSource); + MOZ_ASSERT(mSourceAsObjectStore || mSourceAsIndex); + MOZ_ASSERT(!mSourceAsCursor); + + mSourceAsCursor = aSource; +} + +JSObject* +IDBRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return IDBRequestBinding::Wrap(aCx, this, aGivenProto); +} + +void +IDBRequest::GetResult(JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) const +{ + AssertIsOnOwningThread(); + + if (!mHaveResultOrErrorCode) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + aResult.set(mResultVal); +} + +void +IDBRequest::SetResultCallback(ResultCallback* aCallback) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mHaveResultOrErrorCode); + MOZ_ASSERT(mResultVal.isUndefined()); + MOZ_ASSERT(!mError); + + // See if our window is still valid. + if (NS_WARN_IF(NS_FAILED(CheckInnerWindowCorrectness()))) { + SetError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + AutoJSAPI autoJS; + Maybe<JSAutoCompartment> ac; + + if (GetScriptOwner()) { + // If we have a script owner we want the SafeJSContext and then to enter the + // script owner's compartment. + autoJS.Init(); + ac.emplace(autoJS.cx(), GetScriptOwner()); + } else { + // Otherwise our owner is a window and we use that to initialize. + MOZ_ASSERT(GetOwner()); + if (!autoJS.Init(GetOwner())) { + IDB_WARNING("Failed to initialize AutoJSAPI!"); + SetError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + } + + JSContext* cx = autoJS.cx(); + + AssertIsRooted(); + + JS::Rooted<JS::Value> result(cx); + nsresult rv = aCallback->GetResult(cx, &result); + if (NS_WARN_IF(NS_FAILED(rv))) { + // This can only fail if the structured clone contains a mutable file + // and the child is not in the main thread and main process. + // In that case CreateAndWrapMutableFile() returns false which shows up + // as NS_ERROR_DOM_DATA_CLONE_ERR here. + MOZ_ASSERT(rv == NS_ERROR_DOM_DATA_CLONE_ERR); + + // We are not setting a result or an error object here since we want to + // throw an exception when the 'result' property is being touched. + return; + } + + mError = nullptr; + mResultVal = result; + + mHaveResultOrErrorCode = true; +} + +DOMError* +IDBRequest::GetError(ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (!mHaveResultOrErrorCode) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + return mError; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBRequest) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBRequest, IDBWrapperCache) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS because + // DOMEventTargetHelper does it for us. + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceAsObjectStore) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceAsIndex) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceAsCursor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBRequest, IDBWrapperCache) + tmp->mResultVal.setUndefined(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceAsObjectStore) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceAsIndex) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceAsCursor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(IDBRequest, IDBWrapperCache) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultVal) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBRequest) + if (aIID.Equals(kIDBRequestIID)) { + foundInterface = this; + } else +NS_INTERFACE_MAP_END_INHERITING(IDBWrapperCache) + +NS_IMPL_ADDREF_INHERITED(IDBRequest, IDBWrapperCache) +NS_IMPL_RELEASE_INHERITED(IDBRequest, IDBWrapperCache) + +nsresult +IDBRequest::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + AssertIsOnOwningThread(); + + aVisitor.mCanHandle = true; + aVisitor.mParentTarget = mTransaction; + return NS_OK; +} + +class IDBOpenDBRequest::WorkerHolder final + : public mozilla::dom::workers::WorkerHolder +{ + WorkerPrivate* mWorkerPrivate; +#ifdef DEBUG + // This is only here so that assertions work in the destructor even if + // NoteAddWorkerHolderFailed was called. + WorkerPrivate* mWorkerPrivateDEBUG; +#endif + +public: + explicit + WorkerHolder(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) +#ifdef DEBUG + , mWorkerPrivateDEBUG(aWorkerPrivate) +#endif + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_COUNT_CTOR(IDBOpenDBRequest::WorkerHolder); + } + + ~WorkerHolder() + { +#ifdef DEBUG + mWorkerPrivateDEBUG->AssertIsOnWorkerThread(); +#endif + + MOZ_COUNT_DTOR(IDBOpenDBRequest::WorkerHolder); + } + + void + NoteAddWorkerHolderFailed() + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + mWorkerPrivate = nullptr; + } + +private: + virtual bool + Notify(Status aStatus) override; +}; + +IDBOpenDBRequest::IDBOpenDBRequest(IDBFactory* aFactory, + nsPIDOMWindowInner* aOwner, + bool aFileHandleDisabled) + : IDBRequest(aOwner) + , mFactory(aFactory) + , mFileHandleDisabled(aFileHandleDisabled) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aFactory); + + // aOwner may be null. +} + +IDBOpenDBRequest::~IDBOpenDBRequest() +{ + AssertIsOnOwningThread(); +} + +// static +already_AddRefed<IDBOpenDBRequest> +IDBOpenDBRequest::CreateForWindow(JSContext* aCx, + IDBFactory* aFactory, + nsPIDOMWindowInner* aOwner, + JS::Handle<JSObject*> aScriptOwner) +{ + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aOwner); + MOZ_ASSERT(aScriptOwner); + + bool fileHandleDisabled = !IndexedDatabaseManager::IsFileHandleEnabled(); + + RefPtr<IDBOpenDBRequest> request = + new IDBOpenDBRequest(aFactory, aOwner, fileHandleDisabled); + CaptureCaller(aCx, request->mFilename, &request->mLineNo, &request->mColumn); + + request->SetScriptOwner(aScriptOwner); + + return request.forget(); +} + +// static +already_AddRefed<IDBOpenDBRequest> +IDBOpenDBRequest::CreateForJS(JSContext* aCx, + IDBFactory* aFactory, + JS::Handle<JSObject*> aScriptOwner) +{ + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aScriptOwner); + + bool fileHandleDisabled = !IndexedDatabaseManager::IsFileHandleEnabled(); + + RefPtr<IDBOpenDBRequest> request = + new IDBOpenDBRequest(aFactory, nullptr, fileHandleDisabled); + CaptureCaller(aCx, request->mFilename, &request->mLineNo, &request->mColumn); + + request->SetScriptOwner(aScriptOwner); + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + workerPrivate->AssertIsOnWorkerThread(); + + nsAutoPtr<WorkerHolder> workerHolder(new WorkerHolder(workerPrivate)); + if (NS_WARN_IF(!workerHolder->HoldWorker(workerPrivate, Canceling))) { + workerHolder->NoteAddWorkerHolderFailed(); + return nullptr; + } + + request->mWorkerHolder = Move(workerHolder); + } + + return request.forget(); +} + +void +IDBOpenDBRequest::SetTransaction(IDBTransaction* aTransaction) +{ + AssertIsOnOwningThread(); + + MOZ_ASSERT(!aTransaction || !mTransaction); + + mTransaction = aTransaction; +} + +void +IDBOpenDBRequest::NoteComplete() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(!NS_IsMainThread(), mWorkerHolder); + + // If we have a WorkerHolder installed on the worker then nulling this out + // will uninstall it from the worker. + mWorkerHolder = nullptr; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBOpenDBRequest) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBOpenDBRequest, + IDBRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFactory) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBOpenDBRequest, + IDBRequest) + // Don't unlink mFactory! +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBOpenDBRequest) +NS_INTERFACE_MAP_END_INHERITING(IDBRequest) + +NS_IMPL_ADDREF_INHERITED(IDBOpenDBRequest, IDBRequest) +NS_IMPL_RELEASE_INHERITED(IDBOpenDBRequest, IDBRequest) + +nsresult +IDBOpenDBRequest::PostHandleEvent(EventChainPostVisitor& aVisitor) +{ + nsresult rv = + IndexedDatabaseManager::CommonPostHandleEvent(aVisitor, mFactory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +JSObject* +IDBOpenDBRequest::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnOwningThread(); + + return IDBOpenDBRequestBinding::Wrap(aCx, this, aGivenProto); +} + +bool +IDBOpenDBRequest:: +WorkerHolder::Notify(Status aStatus) +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aStatus > Running); + + // There's nothing we can really do here at the moment... + NS_WARNING("Worker closing but IndexedDB is waiting to open a database!"); + + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBRequest.h b/dom/indexedDB/IDBRequest.h new file mode 100644 index 000000000..1c7fca756 --- /dev/null +++ b/dom/indexedDB/IDBRequest.h @@ -0,0 +1,293 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbrequest_h__ +#define mozilla_dom_idbrequest_h__ + +#include "js/RootingAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/dom/IDBRequestBinding.h" +#include "mozilla/dom/IDBWrapperCache.h" +#include "nsAutoPtr.h" +#include "nsCycleCollectionParticipant.h" + +#define PRIVATE_IDBREQUEST_IID \ + {0xe68901e5, 0x1d50, 0x4ee9, {0xaf, 0x49, 0x90, 0x99, 0x4a, 0xff, 0xc8, 0x39}} + +class nsPIDOMWindowInner; +struct PRThread; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class DOMError; +class IDBCursor; +class IDBDatabase; +class IDBFactory; +class IDBIndex; +class IDBObjectStore; +class IDBTransaction; +template <typename> struct Nullable; +class OwningIDBObjectStoreOrIDBIndexOrIDBCursor; + +class IDBRequest + : public IDBWrapperCache +{ +protected: + // mSourceAsObjectStore and mSourceAsIndex are exclusive and one must always + // be set. mSourceAsCursor is sometimes set also. + RefPtr<IDBObjectStore> mSourceAsObjectStore; + RefPtr<IDBIndex> mSourceAsIndex; + RefPtr<IDBCursor> mSourceAsCursor; + + RefPtr<IDBTransaction> mTransaction; + +#ifdef DEBUG + PRThread* mOwningThread; +#endif + + JS::Heap<JS::Value> mResultVal; + RefPtr<DOMError> mError; + + nsString mFilename; + uint64_t mLoggingSerialNumber; + nsresult mErrorCode; + uint32_t mLineNo; + uint32_t mColumn; + bool mHaveResultOrErrorCode; + +public: + class ResultCallback; + + static already_AddRefed<IDBRequest> + Create(JSContext* aCx, IDBDatabase* aDatabase, IDBTransaction* aTransaction); + + static already_AddRefed<IDBRequest> + Create(JSContext* aCx, + IDBObjectStore* aSource, + IDBDatabase* aDatabase, + IDBTransaction* aTransaction); + + static already_AddRefed<IDBRequest> + Create(JSContext* aCx, + IDBIndex* aSource, + IDBDatabase* aDatabase, + IDBTransaction* aTransaction); + + static void + CaptureCaller(JSContext* aCx, nsAString& aFilename, uint32_t* aLineNo, + uint32_t* aColumn); + + static uint64_t + NextSerialNumber(); + + // nsIDOMEventTarget + virtual nsresult + PreHandleEvent(EventChainPreVisitor& aVisitor) override; + + void + GetSource(Nullable<OwningIDBObjectStoreOrIDBIndexOrIDBCursor>& aSource) const; + + void + Reset(); + + void + DispatchNonTransactionError(nsresult aErrorCode); + + void + SetResultCallback(ResultCallback* aCallback); + + void + SetError(nsresult aRv); + + nsresult + GetErrorCode() const +#ifdef DEBUG + ; +#else + { + return mErrorCode; + } +#endif + + DOMError* + GetErrorAfterResult() const +#ifdef DEBUG + ; +#else + { + return mError; + } +#endif + + DOMError* + GetError(ErrorResult& aRv); + + void + GetCallerLocation(nsAString& aFilename, uint32_t* aLineNo, + uint32_t* aColumn) const; + + bool + IsPending() const + { + return !mHaveResultOrErrorCode; + } + + uint64_t + LoggingSerialNumber() const + { + AssertIsOnOwningThread(); + + return mLoggingSerialNumber; + } + + void + SetLoggingSerialNumber(uint64_t aLoggingSerialNumber); + + nsPIDOMWindowInner* + GetParentObject() const + { + return GetOwner(); + } + + void + GetResult(JS::MutableHandle<JS::Value> aResult, ErrorResult& aRv) const; + + void + GetResult(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) const + { + GetResult(aResult, aRv); + } + + IDBTransaction* + GetTransaction() const + { + AssertIsOnOwningThread(); + + return mTransaction; + } + + IDBRequestReadyState + ReadyState() const; + + void + SetSource(IDBCursor* aSource); + + IMPL_EVENT_HANDLER(success); + IMPL_EVENT_HANDLER(error); + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(IDBRequest, + IDBWrapperCache) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +protected: + explicit IDBRequest(IDBDatabase* aDatabase); + explicit IDBRequest(nsPIDOMWindowInner* aOwner); + ~IDBRequest(); + + void + InitMembers(); + + void + ConstructResult(); +}; + +class NS_NO_VTABLE IDBRequest::ResultCallback +{ +public: + virtual nsresult + GetResult(JSContext* aCx, JS::MutableHandle<JS::Value> aResult) = 0; + +protected: + ResultCallback() + { } +}; + +class IDBOpenDBRequest final + : public IDBRequest +{ + class WorkerHolder; + + // Only touched on the owning thread. + RefPtr<IDBFactory> mFactory; + + nsAutoPtr<WorkerHolder> mWorkerHolder; + + const bool mFileHandleDisabled; + +public: + static already_AddRefed<IDBOpenDBRequest> + CreateForWindow(JSContext* aCx, + IDBFactory* aFactory, + nsPIDOMWindowInner* aOwner, + JS::Handle<JSObject*> aScriptOwner); + + static already_AddRefed<IDBOpenDBRequest> + CreateForJS(JSContext* aCx, + IDBFactory* aFactory, + JS::Handle<JSObject*> aScriptOwner); + + bool + IsFileHandleDisabled() const + { + return mFileHandleDisabled; + } + + void + SetTransaction(IDBTransaction* aTransaction); + + void + NoteComplete(); + + // nsIDOMEventTarget + virtual nsresult + PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + IDBFactory* + Factory() const + { + return mFactory; + } + + IMPL_EVENT_HANDLER(blocked); + IMPL_EVENT_HANDLER(upgradeneeded); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBOpenDBRequest, IDBRequest) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +private: + IDBOpenDBRequest(IDBFactory* aFactory, + nsPIDOMWindowInner* aOwner, + bool aFileHandleDisabled); + + ~IDBOpenDBRequest(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbrequest_h__ diff --git a/dom/indexedDB/IDBTransaction.cpp b/dom/indexedDB/IDBTransaction.cpp new file mode 100644 index 000000000..a50489898 --- /dev/null +++ b/dom/indexedDB/IDBTransaction.cpp @@ -0,0 +1,1049 @@ +/* -*- 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 "IDBTransaction.h" + +#include "BackgroundChildImpl.h" +#include "IDBDatabase.h" +#include "IDBEvents.h" +#include "IDBObjectStore.h" +#include "IDBRequest.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "nsAutoPtr.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" +#include "nsTHashtable.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "WorkerHolder.h" +#include "WorkerPrivate.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::dom::workers; +using namespace mozilla::ipc; + +class IDBTransaction::WorkerHolder final + : public mozilla::dom::workers::WorkerHolder +{ + WorkerPrivate* mWorkerPrivate; + + // The IDBTransaction owns this object so we only need a weak reference back + // to it. + IDBTransaction* mTransaction; + +public: + WorkerHolder(WorkerPrivate* aWorkerPrivate, IDBTransaction* aTransaction) + : mWorkerPrivate(aWorkerPrivate) + , mTransaction(aTransaction) + { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aTransaction); + aWorkerPrivate->AssertIsOnWorkerThread(); + aTransaction->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(IDBTransaction::WorkerHolder); + } + + ~WorkerHolder() + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_COUNT_DTOR(IDBTransaction::WorkerHolder); + } + +private: + virtual bool + Notify(Status aStatus) override; +}; + +IDBTransaction::IDBTransaction(IDBDatabase* aDatabase, + const nsTArray<nsString>& aObjectStoreNames, + Mode aMode) + : IDBWrapperCache(aDatabase) + , mDatabase(aDatabase) + , mObjectStoreNames(aObjectStoreNames) + , mLoggingSerialNumber(0) + , mNextObjectStoreId(0) + , mNextIndexId(0) + , mAbortCode(NS_OK) + , mPendingRequestCount(0) + , mLineNo(0) + , mColumn(0) + , mReadyState(IDBTransaction::INITIAL) + , mMode(aMode) + , mCreating(false) + , mRegistered(false) + , mAbortedByScript(false) +#ifdef DEBUG + , mSentCommitOrAbort(false) + , mFiredCompleteOrAbort(false) +#endif +{ + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + mBackgroundActor.mNormalBackgroundActor = nullptr; + + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + ThreadLocal* idbThreadLocal = threadLocal->mIndexedDBThreadLocal; + MOZ_ASSERT(idbThreadLocal); + + const_cast<int64_t&>(mLoggingSerialNumber) = + idbThreadLocal->NextTransactionSN(aMode); + +#ifdef DEBUG + if (!aObjectStoreNames.IsEmpty()) { + nsTArray<nsString> sortedNames(aObjectStoreNames); + sortedNames.Sort(); + + const uint32_t count = sortedNames.Length(); + MOZ_ASSERT(count == aObjectStoreNames.Length()); + + // Make sure the array is properly sorted. + for (uint32_t index = 0; index < count; index++) { + MOZ_ASSERT(aObjectStoreNames[index] == sortedNames[index]); + } + + // Make sure there are no duplicates in our objectStore names. + for (uint32_t index = 0; index < count - 1; index++) { + MOZ_ASSERT(sortedNames[index] != sortedNames[index + 1]); + } + } +#endif +} + +IDBTransaction::~IDBTransaction() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mPendingRequestCount); + MOZ_ASSERT(!mCreating); + MOZ_ASSERT(mSentCommitOrAbort); + MOZ_ASSERT_IF(mMode == VERSION_CHANGE && + mBackgroundActor.mVersionChangeBackgroundActor, + mFiredCompleteOrAbort); + MOZ_ASSERT_IF(mMode != VERSION_CHANGE && + mBackgroundActor.mNormalBackgroundActor, + mFiredCompleteOrAbort); + + if (mRegistered) { + mDatabase->UnregisterTransaction(this); +#ifdef DEBUG + mRegistered = false; +#endif + } + + if (mMode == VERSION_CHANGE) { + if (auto* actor = mBackgroundActor.mVersionChangeBackgroundActor) { + actor->SendDeleteMeInternal(/* aFailedConstructor */ false); + + MOZ_ASSERT(!mBackgroundActor.mVersionChangeBackgroundActor, + "SendDeleteMeInternal should have cleared!"); + } + } else if (auto* actor = mBackgroundActor.mNormalBackgroundActor) { + actor->SendDeleteMeInternal(); + + MOZ_ASSERT(!mBackgroundActor.mNormalBackgroundActor, + "SendDeleteMeInternal should have cleared!"); + } +} + +// static +already_AddRefed<IDBTransaction> +IDBTransaction::CreateVersionChange( + IDBDatabase* aDatabase, + BackgroundVersionChangeTransactionChild* aActor, + IDBOpenDBRequest* aOpenRequest, + int64_t aNextObjectStoreId, + int64_t aNextIndexId) +{ + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aOpenRequest); + MOZ_ASSERT(aNextObjectStoreId > 0); + MOZ_ASSERT(aNextIndexId > 0); + + nsTArray<nsString> emptyObjectStoreNames; + + RefPtr<IDBTransaction> transaction = + new IDBTransaction(aDatabase, + emptyObjectStoreNames, + VERSION_CHANGE); + aOpenRequest->GetCallerLocation(transaction->mFilename, + &transaction->mLineNo, &transaction->mColumn); + + transaction->SetScriptOwner(aDatabase->GetScriptOwner()); + + nsCOMPtr<nsIRunnable> runnable = do_QueryObject(transaction); + nsContentUtils::RunInMetastableState(runnable.forget()); + + transaction->mBackgroundActor.mVersionChangeBackgroundActor = aActor; + transaction->mNextObjectStoreId = aNextObjectStoreId; + transaction->mNextIndexId = aNextIndexId; + transaction->mCreating = true; + + aDatabase->RegisterTransaction(transaction); + transaction->mRegistered = true; + + return transaction.forget(); +} + +// static +already_AddRefed<IDBTransaction> +IDBTransaction::Create(JSContext* aCx, IDBDatabase* aDatabase, + const nsTArray<nsString>& aObjectStoreNames, + Mode aMode) +{ + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); + MOZ_ASSERT(aMode == READ_ONLY || + aMode == READ_WRITE || + aMode == READ_WRITE_FLUSH || + aMode == CLEANUP); + + RefPtr<IDBTransaction> transaction = + new IDBTransaction(aDatabase, aObjectStoreNames, aMode); + IDBRequest::CaptureCaller(aCx, transaction->mFilename, &transaction->mLineNo, + &transaction->mColumn); + + transaction->SetScriptOwner(aDatabase->GetScriptOwner()); + + nsCOMPtr<nsIRunnable> runnable = do_QueryObject(transaction); + nsContentUtils::RunInMetastableState(runnable.forget()); + + transaction->mCreating = true; + + aDatabase->RegisterTransaction(transaction); + transaction->mRegistered = true; + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + workerPrivate->AssertIsOnWorkerThread(); + + transaction->mWorkerHolder = new WorkerHolder(workerPrivate, transaction); + MOZ_ALWAYS_TRUE(transaction->mWorkerHolder->HoldWorker(workerPrivate, Canceling)); + } + + return transaction.forget(); +} + +// static +IDBTransaction* +IDBTransaction::GetCurrent() +{ + using namespace mozilla::ipc; + + MOZ_ASSERT(BackgroundChild::GetForCurrentThread()); + + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + ThreadLocal* idbThreadLocal = threadLocal->mIndexedDBThreadLocal; + MOZ_ASSERT(idbThreadLocal); + + return idbThreadLocal->GetCurrentTransaction(); +} + +#ifdef DEBUG + +void +IDBTransaction::AssertIsOnOwningThread() const +{ + MOZ_ASSERT(mDatabase); + mDatabase->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void +IDBTransaction::SetBackgroundActor(indexedDB::BackgroundTransactionChild* aBackgroundActor) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor); + MOZ_ASSERT(!mBackgroundActor.mNormalBackgroundActor); + MOZ_ASSERT(mMode != VERSION_CHANGE); + + mBackgroundActor.mNormalBackgroundActor = aBackgroundActor; +} + +BackgroundRequestChild* +IDBTransaction::StartRequest(IDBRequest* aRequest, const RequestParams& aParams) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + BackgroundRequestChild* actor = new BackgroundRequestChild(aRequest); + + if (mMode == VERSION_CHANGE) { + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + + mBackgroundActor.mVersionChangeBackgroundActor-> + SendPBackgroundIDBRequestConstructor(actor, aParams); + } else { + MOZ_ASSERT(mBackgroundActor.mNormalBackgroundActor); + + mBackgroundActor.mNormalBackgroundActor-> + SendPBackgroundIDBRequestConstructor(actor, aParams); + } + + // Balanced in BackgroundRequestChild::Recv__delete__(). + OnNewRequest(); + + return actor; +} + +void +IDBTransaction::OpenCursor(BackgroundCursorChild* aBackgroundActor, + const OpenCursorParams& aParams) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + if (mMode == VERSION_CHANGE) { + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + + mBackgroundActor.mVersionChangeBackgroundActor-> + SendPBackgroundIDBCursorConstructor(aBackgroundActor, aParams); + } else { + MOZ_ASSERT(mBackgroundActor.mNormalBackgroundActor); + + mBackgroundActor.mNormalBackgroundActor-> + SendPBackgroundIDBCursorConstructor(aBackgroundActor, aParams); + } + + // Balanced in BackgroundCursorChild::RecvResponse(). + OnNewRequest(); +} + +void +IDBTransaction::RefreshSpec(bool aMayDelete) +{ + AssertIsOnOwningThread(); + + for (uint32_t count = mObjectStores.Length(), index = 0; + index < count; + index++) { + mObjectStores[index]->RefreshSpec(aMayDelete); + } + + for (uint32_t count = mDeletedObjectStores.Length(), index = 0; + index < count; + index++) { + mDeletedObjectStores[index]->RefreshSpec(false); + } +} + +void +IDBTransaction::OnNewRequest() +{ + AssertIsOnOwningThread(); + + if (!mPendingRequestCount) { + MOZ_ASSERT(INITIAL == mReadyState); + mReadyState = LOADING; + } + + ++mPendingRequestCount; +} + +void +IDBTransaction::OnRequestFinished(bool aActorDestroyedNormally) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(mPendingRequestCount); + + --mPendingRequestCount; + + if (!mPendingRequestCount) { + mReadyState = COMMITTING; + + if (aActorDestroyedNormally) { + if (NS_SUCCEEDED(mAbortCode)) { + SendCommit(); + } else { + SendAbort(mAbortCode); + } + } else { + // Don't try to send any more messages to the parent if the request actor + // was killed. +#ifdef DEBUG + MOZ_ASSERT(!mSentCommitOrAbort); + mSentCommitOrAbort = true; +#endif + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld]: " + "Request actor was killed, transaction will be aborted", + "IndexedDB %s: C T[%lld]: IDBTransaction abort", + IDB_LOG_ID_STRING(), + LoggingSerialNumber()); + } + } +} + +void +IDBTransaction::SendCommit() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_SUCCEEDED(mAbortCode)); + MOZ_ASSERT(IsCommittingOrDone()); + MOZ_ASSERT(!mSentCommitOrAbort); + MOZ_ASSERT(!mPendingRequestCount); + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "All requests complete, committing transaction", + "IndexedDB %s: C T[%lld] R[%llu]: IDBTransaction commit", + IDB_LOG_ID_STRING(), + LoggingSerialNumber(), + requestSerialNumber); + + if (mMode == VERSION_CHANGE) { + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + mBackgroundActor.mVersionChangeBackgroundActor->SendCommit(); + } else { + MOZ_ASSERT(mBackgroundActor.mNormalBackgroundActor); + mBackgroundActor.mNormalBackgroundActor->SendCommit(); + } + +#ifdef DEBUG + mSentCommitOrAbort = true; +#endif +} + +void +IDBTransaction::SendAbort(nsresult aResultCode) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); + MOZ_ASSERT(IsCommittingOrDone()); + MOZ_ASSERT(!mSentCommitOrAbort); + + // Don't do this in the macro because we always need to increment the serial + // number to keep in sync with the parent. + const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber(); + + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld] Request[%llu]: " + "Aborting transaction with result 0x%x", + "IndexedDB %s: C T[%lld] R[%llu]: IDBTransaction abort (0x%x)", + IDB_LOG_ID_STRING(), + LoggingSerialNumber(), + requestSerialNumber, + aResultCode); + + if (mMode == VERSION_CHANGE) { + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + mBackgroundActor.mVersionChangeBackgroundActor->SendAbort(aResultCode); + } else { + MOZ_ASSERT(mBackgroundActor.mNormalBackgroundActor); + mBackgroundActor.mNormalBackgroundActor->SendAbort(aResultCode); + } + +#ifdef DEBUG + mSentCommitOrAbort = true; +#endif +} + +bool +IDBTransaction::IsOpen() const +{ + AssertIsOnOwningThread(); + + // If we haven't started anything then we're open. + if (mReadyState == IDBTransaction::INITIAL) { + return true; + } + + // If we've already started then we need to check to see if we still have the + // mCreating flag set. If we do (i.e. we haven't returned to the event loop + // from the time we were created) then we are open. Otherwise check the + // currently running transaction to see if it's the same. We only allow other + // requests to be made if this transaction is currently running. + if (mReadyState == IDBTransaction::LOADING && + (mCreating || GetCurrent() == this)) { + return true; + } + + return false; +} + +void +IDBTransaction::GetCallerLocation(nsAString& aFilename, uint32_t* aLineNo, + uint32_t* aColumn) const +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aLineNo); + MOZ_ASSERT(aColumn); + + aFilename = mFilename; + *aLineNo = mLineNo; + *aColumn = mColumn; +} + +already_AddRefed<IDBObjectStore> +IDBTransaction::CreateObjectStore(const ObjectStoreSpec& aSpec) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aSpec.metadata().id()); + MOZ_ASSERT(VERSION_CHANGE == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsOpen()); + +#ifdef DEBUG + { + const nsString& name = aSpec.metadata().name(); + + for (uint32_t count = mObjectStores.Length(), index = 0; + index < count; + index++) { + MOZ_ASSERT(mObjectStores[index]->Name() != name); + } + } +#endif + + MOZ_ALWAYS_TRUE(mBackgroundActor.mVersionChangeBackgroundActor-> + SendCreateObjectStore(aSpec.metadata())); + + RefPtr<IDBObjectStore> objectStore = IDBObjectStore::Create(this, aSpec); + MOZ_ASSERT(objectStore); + + mObjectStores.AppendElement(objectStore); + + return objectStore.forget(); +} + +void +IDBTransaction::DeleteObjectStore(int64_t aObjectStoreId) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(VERSION_CHANGE == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsOpen()); + + MOZ_ALWAYS_TRUE(mBackgroundActor.mVersionChangeBackgroundActor-> + SendDeleteObjectStore(aObjectStoreId)); + + for (uint32_t count = mObjectStores.Length(), index = 0; + index < count; + index++) { + RefPtr<IDBObjectStore>& objectStore = mObjectStores[index]; + + if (objectStore->Id() == aObjectStoreId) { + objectStore->NoteDeletion(); + + RefPtr<IDBObjectStore>* deletedObjectStore = + mDeletedObjectStores.AppendElement(); + deletedObjectStore->swap(mObjectStores[index]); + + mObjectStores.RemoveElementAt(index); + break; + } + } +} + +void +IDBTransaction::RenameObjectStore(int64_t aObjectStoreId, + const nsAString& aName) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(VERSION_CHANGE == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsOpen()); + + MOZ_ALWAYS_TRUE(mBackgroundActor.mVersionChangeBackgroundActor-> + SendRenameObjectStore(aObjectStoreId, nsString(aName))); +} + +void +IDBTransaction::CreateIndex(IDBObjectStore* aObjectStore, + const indexedDB::IndexMetadata& aMetadata) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStore); + MOZ_ASSERT(aMetadata.id()); + MOZ_ASSERT(VERSION_CHANGE == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsOpen()); + + MOZ_ALWAYS_TRUE(mBackgroundActor.mVersionChangeBackgroundActor-> + SendCreateIndex(aObjectStore->Id(), aMetadata)); +} + +void +IDBTransaction::DeleteIndex(IDBObjectStore* aObjectStore, + int64_t aIndexId) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStore); + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(VERSION_CHANGE == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsOpen()); + + MOZ_ALWAYS_TRUE(mBackgroundActor.mVersionChangeBackgroundActor-> + SendDeleteIndex(aObjectStore->Id(), aIndexId)); +} + +void +IDBTransaction::RenameIndex(IDBObjectStore* aObjectStore, + int64_t aIndexId, + const nsAString& aName) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStore); + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(VERSION_CHANGE == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsOpen()); + + MOZ_ALWAYS_TRUE(mBackgroundActor.mVersionChangeBackgroundActor-> + SendRenameIndex(aObjectStore->Id(), + aIndexId, + nsString(aName))); +} + +void +IDBTransaction::AbortInternal(nsresult aAbortCode, + already_AddRefed<DOMError> aError) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aAbortCode)); + MOZ_ASSERT(!IsCommittingOrDone()); + + RefPtr<DOMError> error = aError; + + const bool isVersionChange = mMode == VERSION_CHANGE; + const bool isInvalidated = mDatabase->IsInvalidated(); + bool needToSendAbort = mReadyState == INITIAL; + + mAbortCode = aAbortCode; + mReadyState = DONE; + mError = error.forget(); + + if (isVersionChange) { + // If a version change transaction is aborted, we must revert the world + // back to its previous state unless we're being invalidated after the + // transaction already completed. + if (!isInvalidated) { + mDatabase->RevertToPreviousState(); + } + + // We do the reversion only for the mObjectStores/mDeletedObjectStores but + // not for the mIndexes/mDeletedIndexes of each IDBObjectStore because it's + // time-consuming(O(m*n)) and mIndexes/mDeletedIndexes won't be used anymore + // in IDBObjectStore::(Create|Delete)Index() and IDBObjectStore::Index() in + // which all the executions are returned earlier by !transaction->IsOpen(). + + const nsTArray<ObjectStoreSpec>& specArray = + mDatabase->Spec()->objectStores(); + + if (specArray.IsEmpty()) { + mObjectStores.Clear(); + mDeletedObjectStores.Clear(); + } else { + nsTHashtable<nsUint64HashKey> validIds(specArray.Length()); + + for (uint32_t specCount = specArray.Length(), specIndex = 0; + specIndex < specCount; + specIndex++) { + const int64_t objectStoreId = specArray[specIndex].metadata().id(); + MOZ_ASSERT(objectStoreId); + + validIds.PutEntry(uint64_t(objectStoreId)); + } + + for (uint32_t objCount = mObjectStores.Length(), objIndex = 0; + objIndex < objCount; + /* incremented conditionally */) { + const int64_t objectStoreId = mObjectStores[objIndex]->Id(); + MOZ_ASSERT(objectStoreId); + + if (validIds.Contains(uint64_t(objectStoreId))) { + objIndex++; + } else { + mObjectStores.RemoveElementAt(objIndex); + objCount--; + } + } + + if (!mDeletedObjectStores.IsEmpty()) { + for (uint32_t objCount = mDeletedObjectStores.Length(), objIndex = 0; + objIndex < objCount; + objIndex++) { + const int64_t objectStoreId = mDeletedObjectStores[objIndex]->Id(); + MOZ_ASSERT(objectStoreId); + + if (validIds.Contains(uint64_t(objectStoreId))) { + RefPtr<IDBObjectStore>* objectStore = + mObjectStores.AppendElement(); + objectStore->swap(mDeletedObjectStores[objIndex]); + } + } + mDeletedObjectStores.Clear(); + } + } + } + + // Fire the abort event if there are no outstanding requests. Otherwise the + // abort event will be fired when all outstanding requests finish. + if (needToSendAbort) { + SendAbort(aAbortCode); + } + + if (isVersionChange) { + mDatabase->Close(); + } +} + +void +IDBTransaction::Abort(IDBRequest* aRequest) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(aRequest); + + if (IsCommittingOrDone()) { + // Already started (and maybe finished) the commit or abort so there is + // nothing to do here. + return; + } + + ErrorResult rv; + RefPtr<DOMError> error = aRequest->GetError(rv); + + AbortInternal(aRequest->GetErrorCode(), error.forget()); +} + +void +IDBTransaction::Abort(nsresult aErrorCode) +{ + AssertIsOnOwningThread(); + + if (IsCommittingOrDone()) { + // Already started (and maybe finished) the commit or abort so there is + // nothing to do here. + return; + } + + RefPtr<DOMError> error = new DOMError(GetOwner(), aErrorCode); + AbortInternal(aErrorCode, error.forget()); +} + +void +IDBTransaction::Abort(ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (IsCommittingOrDone()) { + aRv = NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + return; + } + + AbortInternal(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, nullptr); + + MOZ_ASSERT(!mAbortedByScript); + mAbortedByScript = true; +} + +void +IDBTransaction::FireCompleteOrAbortEvents(nsresult aResult) +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!mFiredCompleteOrAbort); + + mReadyState = DONE; + +#ifdef DEBUG + mFiredCompleteOrAbort = true; +#endif + + // Make sure we drop the WorkerHolder when this function completes. + nsAutoPtr<WorkerHolder> workerHolder = Move(mWorkerHolder); + + nsCOMPtr<nsIDOMEvent> event; + if (NS_SUCCEEDED(aResult)) { + event = CreateGenericEvent(this, + nsDependentString(kCompleteEventType), + eDoesNotBubble, + eNotCancelable); + MOZ_ASSERT(event); + } else { + if (aResult == NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR) { + mDatabase->SetQuotaExceeded(); + } + + if (!mError && !mAbortedByScript) { + mError = new DOMError(GetOwner(), aResult); + } + + event = CreateGenericEvent(this, + nsDependentString(kAbortEventType), + eDoesBubble, + eNotCancelable); + MOZ_ASSERT(event); + } + + if (NS_SUCCEEDED(mAbortCode)) { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld]: " + "Firing 'complete' event", + "IndexedDB %s: C T[%lld]: IDBTransaction 'complete' event", + IDB_LOG_ID_STRING(), + mLoggingSerialNumber); + } else { + IDB_LOG_MARK("IndexedDB %s: Child Transaction[%lld]: " + "Firing 'abort' event with error 0x%x", + "IndexedDB %s: C T[%lld]: IDBTransaction 'abort' event (0x%x)", + IDB_LOG_ID_STRING(), + mLoggingSerialNumber, + mAbortCode); + } + + bool dummy; + if (NS_FAILED(DispatchEvent(event, &dummy))) { + NS_WARNING("DispatchEvent failed!"); + } + + mDatabase->DelayedMaybeExpireFileActors(); +} + +int64_t +IDBTransaction::NextObjectStoreId() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(VERSION_CHANGE == mMode); + + return mNextObjectStoreId++; +} + +int64_t +IDBTransaction::NextIndexId() +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(VERSION_CHANGE == mMode); + + return mNextIndexId++; +} + +nsPIDOMWindowInner* +IDBTransaction::GetParentObject() const +{ + AssertIsOnOwningThread(); + + return mDatabase->GetParentObject(); +} + +IDBTransactionMode +IDBTransaction::GetMode(ErrorResult& aRv) const +{ + AssertIsOnOwningThread(); + + switch (mMode) { + case READ_ONLY: + return IDBTransactionMode::Readonly; + + case READ_WRITE: + return IDBTransactionMode::Readwrite; + + case READ_WRITE_FLUSH: + return IDBTransactionMode::Readwriteflush; + + case CLEANUP: + return IDBTransactionMode::Cleanup; + + case VERSION_CHANGE: + return IDBTransactionMode::Versionchange; + + case MODE_INVALID: + default: + MOZ_CRASH("Bad mode!"); + } +} + +DOMError* +IDBTransaction::GetError() const +{ + AssertIsOnOwningThread(); + + return mError; +} + +already_AddRefed<DOMStringList> +IDBTransaction::ObjectStoreNames() const +{ + AssertIsOnOwningThread(); + + if (mMode == IDBTransaction::VERSION_CHANGE) { + return mDatabase->ObjectStoreNames(); + } + + RefPtr<DOMStringList> list = new DOMStringList(); + list->StringArray() = mObjectStoreNames; + return list.forget(); +} + +already_AddRefed<IDBObjectStore> +IDBTransaction::ObjectStore(const nsAString& aName, ErrorResult& aRv) +{ + AssertIsOnOwningThread(); + + if (IsCommittingOrDone()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + const ObjectStoreSpec* spec = nullptr; + + if (IDBTransaction::VERSION_CHANGE == mMode || + mObjectStoreNames.Contains(aName)) { + const nsTArray<ObjectStoreSpec>& objectStores = + mDatabase->Spec()->objectStores(); + + for (uint32_t count = objectStores.Length(), index = 0; + index < count; + index++) { + const ObjectStoreSpec& objectStore = objectStores[index]; + if (objectStore.metadata().name() == aName) { + spec = &objectStore; + break; + } + } + } + + if (!spec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return nullptr; + } + + const int64_t desiredId = spec->metadata().id(); + + RefPtr<IDBObjectStore> objectStore; + + for (uint32_t count = mObjectStores.Length(), index = 0; + index < count; + index++) { + RefPtr<IDBObjectStore>& existingObjectStore = mObjectStores[index]; + + if (existingObjectStore->Id() == desiredId) { + objectStore = existingObjectStore; + break; + } + } + + if (!objectStore) { + objectStore = IDBObjectStore::Create(this, *spec); + MOZ_ASSERT(objectStore); + + mObjectStores.AppendElement(objectStore); + } + + return objectStore.forget(); +} + +NS_IMPL_ADDREF_INHERITED(IDBTransaction, IDBWrapperCache) +NS_IMPL_RELEASE_INHERITED(IDBTransaction, IDBWrapperCache) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBTransaction) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) +NS_INTERFACE_MAP_END_INHERITING(IDBWrapperCache) + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBTransaction) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBTransaction, + IDBWrapperCache) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDatabase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObjectStores) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDeletedObjectStores) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBTransaction, IDBWrapperCache) + // Don't unlink mDatabase! + NS_IMPL_CYCLE_COLLECTION_UNLINK(mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mObjectStores) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDeletedObjectStores) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +JSObject* +IDBTransaction::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnOwningThread(); + + return IDBTransactionBinding::Wrap(aCx, this, aGivenProto); +} + +nsresult +IDBTransaction::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + AssertIsOnOwningThread(); + + aVisitor.mCanHandle = true; + aVisitor.mParentTarget = mDatabase; + return NS_OK; +} + +NS_IMETHODIMP +IDBTransaction::Run() +{ + AssertIsOnOwningThread(); + + // We're back at the event loop, no longer newborn. + mCreating = false; + + // Maybe commit if there were no requests generated. + if (mReadyState == IDBTransaction::INITIAL) { + mReadyState = DONE; + + SendCommit(); + } + + return NS_OK; +} + +bool +IDBTransaction:: +WorkerHolder::Notify(Status aStatus) +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aStatus > Running); + + if (mTransaction && aStatus > Terminating) { + mTransaction->AssertIsOnOwningThread(); + + RefPtr<IDBTransaction> transaction = Move(mTransaction); + + if (!transaction->IsCommittingOrDone()) { + IDB_REPORT_INTERNAL_ERR(); + transaction->AbortInternal(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, nullptr); + } + } + + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBTransaction.h b/dom/indexedDB/IDBTransaction.h new file mode 100644 index 000000000..1c3e8be99 --- /dev/null +++ b/dom/indexedDB/IDBTransaction.h @@ -0,0 +1,344 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbtransaction_h__ +#define mozilla_dom_idbtransaction_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBTransactionBinding.h" +#include "mozilla/dom/IDBWrapperCache.h" +#include "nsAutoPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIRunnable.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; +class EventChainPreVisitor; + +namespace dom { + +class DOMError; +class DOMStringList; +class IDBDatabase; +class IDBObjectStore; +class IDBOpenDBRequest; +class IDBRequest; + +namespace indexedDB { +class BackgroundCursorChild; +class BackgroundRequestChild; +class BackgroundTransactionChild; +class BackgroundVersionChangeTransactionChild; +class IndexMetadata; +class ObjectStoreSpec; +class OpenCursorParams; +class RequestParams; +} + +class IDBTransaction final + : public IDBWrapperCache + , public nsIRunnable +{ + friend class indexedDB::BackgroundCursorChild; + friend class indexedDB::BackgroundRequestChild; + + class WorkerHolder; + friend class WorkerHolder; + +public: + enum Mode + { + READ_ONLY = 0, + READ_WRITE, + READ_WRITE_FLUSH, + CLEANUP, + VERSION_CHANGE, + + // Only needed for IPC serialization helper, should never be used in code. + MODE_INVALID + }; + + enum ReadyState + { + INITIAL = 0, + LOADING, + COMMITTING, + DONE + }; + +private: + RefPtr<IDBDatabase> mDatabase; + RefPtr<DOMError> mError; + nsTArray<nsString> mObjectStoreNames; + nsTArray<RefPtr<IDBObjectStore>> mObjectStores; + nsTArray<RefPtr<IDBObjectStore>> mDeletedObjectStores; + nsAutoPtr<WorkerHolder> mWorkerHolder; + + // Tagged with mMode. If mMode is VERSION_CHANGE then mBackgroundActor will be + // a BackgroundVersionChangeTransactionChild. Otherwise it will be a + // BackgroundTransactionChild. + union { + indexedDB::BackgroundTransactionChild* mNormalBackgroundActor; + indexedDB::BackgroundVersionChangeTransactionChild* mVersionChangeBackgroundActor; + } mBackgroundActor; + + const int64_t mLoggingSerialNumber; + + // Only used for VERSION_CHANGE transactions. + int64_t mNextObjectStoreId; + int64_t mNextIndexId; + + nsresult mAbortCode; + uint32_t mPendingRequestCount; + + nsString mFilename; + uint32_t mLineNo; + uint32_t mColumn; + + ReadyState mReadyState; + Mode mMode; + + bool mCreating; + bool mRegistered; + bool mAbortedByScript; + +#ifdef DEBUG + bool mSentCommitOrAbort; + bool mFiredCompleteOrAbort; +#endif + +public: + static already_AddRefed<IDBTransaction> + CreateVersionChange(IDBDatabase* aDatabase, + indexedDB::BackgroundVersionChangeTransactionChild* aActor, + IDBOpenDBRequest* aOpenRequest, + int64_t aNextObjectStoreId, + int64_t aNextIndexId); + + static already_AddRefed<IDBTransaction> + Create(JSContext* aCx, IDBDatabase* aDatabase, + const nsTArray<nsString>& aObjectStoreNames, + Mode aMode); + + static IDBTransaction* + GetCurrent(); + + void + AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + void + SetBackgroundActor(indexedDB::BackgroundTransactionChild* aBackgroundActor); + + void + ClearBackgroundActor() + { + AssertIsOnOwningThread(); + + if (mMode == VERSION_CHANGE) { + mBackgroundActor.mVersionChangeBackgroundActor = nullptr; + } else { + mBackgroundActor.mNormalBackgroundActor = nullptr; + } + } + + indexedDB::BackgroundRequestChild* + StartRequest(IDBRequest* aRequest, const indexedDB::RequestParams& aParams); + + void + OpenCursor(indexedDB::BackgroundCursorChild* aBackgroundActor, + const indexedDB::OpenCursorParams& aParams); + + void + RefreshSpec(bool aMayDelete); + + bool + IsOpen() const; + + bool + IsCommittingOrDone() const + { + AssertIsOnOwningThread(); + + return mReadyState == COMMITTING || mReadyState == DONE; + } + + bool + IsDone() const + { + AssertIsOnOwningThread(); + + return mReadyState == DONE; + } + + bool + IsWriteAllowed() const + { + AssertIsOnOwningThread(); + return mMode == READ_WRITE || + mMode == READ_WRITE_FLUSH || + mMode == CLEANUP || + mMode == VERSION_CHANGE; + } + + bool + IsAborted() const + { + AssertIsOnOwningThread(); + return NS_FAILED(mAbortCode); + } + + nsresult + AbortCode() const + { + AssertIsOnOwningThread(); + return mAbortCode; + } + + void + GetCallerLocation(nsAString& aFilename, uint32_t* aLineNo, + uint32_t* aColumn) const; + + // 'Get' prefix is to avoid name collisions with the enum + Mode + GetMode() const + { + AssertIsOnOwningThread(); + return mMode; + } + + IDBDatabase* + Database() const + { + AssertIsOnOwningThread(); + return mDatabase; + } + + IDBDatabase* + Db() const + { + return Database(); + } + + const nsTArray<nsString>& + ObjectStoreNamesInternal() const + { + AssertIsOnOwningThread(); + return mObjectStoreNames; + } + + already_AddRefed<IDBObjectStore> + CreateObjectStore(const indexedDB::ObjectStoreSpec& aSpec); + + void + DeleteObjectStore(int64_t aObjectStoreId); + + void + RenameObjectStore(int64_t aObjectStoreId, const nsAString& aName); + + void + CreateIndex(IDBObjectStore* aObjectStore, const indexedDB::IndexMetadata& aMetadata); + + void + DeleteIndex(IDBObjectStore* aObjectStore, int64_t aIndexId); + + void + RenameIndex(IDBObjectStore* aObjectStore, int64_t aIndexId, const nsAString& aName); + + void + Abort(IDBRequest* aRequest); + + void + Abort(nsresult aAbortCode); + + int64_t + LoggingSerialNumber() const + { + AssertIsOnOwningThread(); + + return mLoggingSerialNumber; + } + + nsPIDOMWindowInner* + GetParentObject() const; + + IDBTransactionMode + GetMode(ErrorResult& aRv) const; + + DOMError* + GetError() const; + + already_AddRefed<IDBObjectStore> + ObjectStore(const nsAString& aName, ErrorResult& aRv); + + void + Abort(ErrorResult& aRv); + + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(complete) + IMPL_EVENT_HANDLER(error) + + already_AddRefed<DOMStringList> + ObjectStoreNames() const; + + void + FireCompleteOrAbortEvents(nsresult aResult); + + // Only for VERSION_CHANGE transactions. + int64_t + NextObjectStoreId(); + + // Only for VERSION_CHANGE transactions. + int64_t + NextIndexId(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBTransaction, IDBWrapperCache) + + // nsWrapperCache + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + // nsIDOMEventTarget + virtual nsresult + PreHandleEvent(EventChainPreVisitor& aVisitor) override; + +private: + IDBTransaction(IDBDatabase* aDatabase, + const nsTArray<nsString>& aObjectStoreNames, + Mode aMode); + ~IDBTransaction(); + + void + AbortInternal(nsresult aAbortCode, already_AddRefed<DOMError> aError); + + void + SendCommit(); + + void + SendAbort(nsresult aResultCode); + + void + OnNewRequest(); + + void + OnRequestFinished(bool aActorDestroyedNormally); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbtransaction_h__ diff --git a/dom/indexedDB/IDBWrapperCache.cpp b/dom/indexedDB/IDBWrapperCache.cpp new file mode 100644 index 000000000..df62514c8 --- /dev/null +++ b/dom/indexedDB/IDBWrapperCache.cpp @@ -0,0 +1,80 @@ +/* -*- 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 "IDBWrapperCache.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "nsCOMPtr.h" +#include "nsIScriptGlobalObject.h" +#include "nsPIDOMWindow.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBWrapperCache) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBWrapperCache, + DOMEventTargetHelper) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS because + // DOMEventTargetHelper does it for us. +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBWrapperCache, + DOMEventTargetHelper) + if (tmp->mScriptOwner) { + tmp->mScriptOwner = nullptr; + mozilla::DropJSObjects(tmp); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(IDBWrapperCache, + DOMEventTargetHelper) + // Don't need NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER because + // DOMEventTargetHelper does it for us. + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScriptOwner) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBWrapperCache) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(IDBWrapperCache, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(IDBWrapperCache, DOMEventTargetHelper) + +IDBWrapperCache::IDBWrapperCache(DOMEventTargetHelper* aOwner) + : DOMEventTargetHelper(aOwner), mScriptOwner(nullptr) +{ } + +IDBWrapperCache::IDBWrapperCache(nsPIDOMWindowInner* aOwner) + : DOMEventTargetHelper(aOwner), mScriptOwner(nullptr) +{ } + +IDBWrapperCache::~IDBWrapperCache() +{ + mScriptOwner = nullptr; + ReleaseWrapper(this); + mozilla::DropJSObjects(this); +} + +void +IDBWrapperCache::SetScriptOwner(JSObject* aScriptOwner) +{ + MOZ_ASSERT(aScriptOwner); + + mScriptOwner = aScriptOwner; + mozilla::HoldJSObjects(this); +} + +#ifdef DEBUG +void +IDBWrapperCache::AssertIsRooted() const +{ + MOZ_ASSERT(IsJSHolder(const_cast<IDBWrapperCache*>(this)), + "Why aren't we rooted?!"); +} +#endif + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IDBWrapperCache.h b/dom/indexedDB/IDBWrapperCache.h new file mode 100644 index 000000000..8f90f3213 --- /dev/null +++ b/dom/indexedDB/IDBWrapperCache.h @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_idbwrappercache_h__ +#define mozilla_dom_idbwrappercache_h__ + +#include "js/RootingAPI.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInnter; + +namespace mozilla { +namespace dom { + +class IDBWrapperCache : public DOMEventTargetHelper +{ + JS::Heap<JSObject*> mScriptOwner; + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(IDBWrapperCache, + DOMEventTargetHelper) + + JSObject* + GetScriptOwner() const + { + return mScriptOwner; + } + + void + SetScriptOwner(JSObject* aScriptOwner); + + void AssertIsRooted() const +#ifdef DEBUG + ; +#else + { } +#endif + +protected: + explicit IDBWrapperCache(DOMEventTargetHelper* aOwner); + explicit IDBWrapperCache(nsPIDOMWindowInner* aOwner); + + virtual ~IDBWrapperCache(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbwrappercache_h__ diff --git a/dom/indexedDB/IndexedDatabase.h b/dom/indexedDB/IndexedDatabase.h new file mode 100644 index 000000000..b0c4cb877 --- /dev/null +++ b/dom/indexedDB/IndexedDatabase.h @@ -0,0 +1,92 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddatabase_h__ +#define mozilla_dom_indexeddatabase_h__ + +#include "js/StructuredClone.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +namespace JS { +struct WasmModule; +} // namespace JS + +namespace mozilla { +namespace dom { + +class Blob; +class IDBDatabase; +class IDBMutableFile; + +namespace indexedDB { + +class FileInfo; +class SerializedStructuredCloneReadInfo; + +struct StructuredCloneFile +{ + enum FileType { + eBlob, + eMutableFile, + eStructuredClone, + eWasmBytecode, + eWasmCompiled, + eEndGuard + }; + + RefPtr<Blob> mBlob; + RefPtr<IDBMutableFile> mMutableFile; + RefPtr<JS::WasmModule> mWasmModule; + RefPtr<FileInfo> mFileInfo; + FileType mType; + + // In IndexedDatabaseInlines.h + inline + StructuredCloneFile(); + + // In IndexedDatabaseInlines.h + inline + ~StructuredCloneFile(); + + // In IndexedDatabaseInlines.h + inline bool + operator==(const StructuredCloneFile& aOther) const; +}; + +struct StructuredCloneReadInfo +{ + JSStructuredCloneData mData; + nsTArray<StructuredCloneFile> mFiles; + IDBDatabase* mDatabase; + bool mHasPreprocessInfo; + + // In IndexedDatabaseInlines.h + inline + StructuredCloneReadInfo(); + + // In IndexedDatabaseInlines.h + inline + ~StructuredCloneReadInfo(); + + // In IndexedDatabaseInlines.h + inline + StructuredCloneReadInfo(StructuredCloneReadInfo&& aOther); + + // In IndexedDatabaseInlines.h + inline StructuredCloneReadInfo& + operator=(StructuredCloneReadInfo&& aOther); + + // In IndexedDatabaseInlines.h + inline + MOZ_IMPLICIT StructuredCloneReadInfo(SerializedStructuredCloneReadInfo&& aOther); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddatabase_h__ diff --git a/dom/indexedDB/IndexedDatabaseInlines.h b/dom/indexedDB/IndexedDatabaseInlines.h new file mode 100644 index 000000000..830c2f110 --- /dev/null +++ b/dom/indexedDB/IndexedDatabaseInlines.h @@ -0,0 +1,106 @@ +/* -*- 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/. */ + +#ifndef IndexedDatabaseInlines_h +#define IndexedDatabaseInlines_h + +#ifndef mozilla_dom_indexeddatabase_h__ +#error Must include IndexedDatabase.h first +#endif + +#include "FileInfo.h" +#include "IDBMutableFile.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/File.h" +#include "nsIInputStream.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +inline +StructuredCloneFile::StructuredCloneFile() + : mType(eBlob) +{ + MOZ_COUNT_CTOR(StructuredCloneFile); +} + +inline +StructuredCloneFile::~StructuredCloneFile() +{ + MOZ_COUNT_DTOR(StructuredCloneFile); +} + +inline +bool +StructuredCloneFile::operator==(const StructuredCloneFile& aOther) const +{ + return this->mBlob == aOther.mBlob && + this->mMutableFile == aOther.mMutableFile && + this->mFileInfo == aOther.mFileInfo && + this->mType == aOther.mType; +} + +inline +StructuredCloneReadInfo::StructuredCloneReadInfo() + : mDatabase(nullptr) + , mHasPreprocessInfo(false) +{ + MOZ_COUNT_CTOR(StructuredCloneReadInfo); +} + +inline +StructuredCloneReadInfo::StructuredCloneReadInfo( + StructuredCloneReadInfo&& aCloneReadInfo) + : mData(Move(aCloneReadInfo.mData)) +{ + MOZ_ASSERT(&aCloneReadInfo != this); + MOZ_COUNT_CTOR(StructuredCloneReadInfo); + + mFiles.Clear(); + mFiles.SwapElements(aCloneReadInfo.mFiles); + mDatabase = aCloneReadInfo.mDatabase; + aCloneReadInfo.mDatabase = nullptr; + mHasPreprocessInfo = aCloneReadInfo.mHasPreprocessInfo; + aCloneReadInfo.mHasPreprocessInfo = false; +} + +inline +StructuredCloneReadInfo::StructuredCloneReadInfo( + SerializedStructuredCloneReadInfo&& aCloneReadInfo) + : mData(Move(aCloneReadInfo.data().data)) + , mDatabase(nullptr) + , mHasPreprocessInfo(aCloneReadInfo.hasPreprocessInfo()) +{ + MOZ_COUNT_CTOR(StructuredCloneReadInfo); +} + +inline +StructuredCloneReadInfo::~StructuredCloneReadInfo() +{ + MOZ_COUNT_DTOR(StructuredCloneReadInfo); +} + +inline StructuredCloneReadInfo& +StructuredCloneReadInfo::operator=(StructuredCloneReadInfo&& aCloneReadInfo) +{ + MOZ_ASSERT(&aCloneReadInfo != this); + + mData = Move(aCloneReadInfo.mData); + mFiles.Clear(); + mFiles.SwapElements(aCloneReadInfo.mFiles); + mDatabase = aCloneReadInfo.mDatabase; + aCloneReadInfo.mDatabase = nullptr; + mHasPreprocessInfo = aCloneReadInfo.mHasPreprocessInfo; + aCloneReadInfo.mHasPreprocessInfo = false; + return *this; +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // IndexedDatabaseInlines_h diff --git a/dom/indexedDB/IndexedDatabaseManager.cpp b/dom/indexedDB/IndexedDatabaseManager.cpp new file mode 100644 index 000000000..2590b0127 --- /dev/null +++ b/dom/indexedDB/IndexedDatabaseManager.cpp @@ -0,0 +1,1461 @@ +/* -*- 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 "IndexedDatabaseManager.h" + +#include "chrome/common/ipc_channel.h" // for IPC::Channel::kMaximumMessageSize +#include "nsIConsoleService.h" +#include "nsIDiskSpaceWatcher.h" +#include "nsIDOMWindow.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsIScriptGlobalObject.h" + +#include "jsapi.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CondVar.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsThreadUtils.h" +#include "mozilla/Logging.h" + +#include "FileInfo.h" +#include "FileManager.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBKeyRange.h" +#include "IDBRequest.h" +#include "ProfilerHelpers.h" +#include "ScriptErrorHelper.h" +#include "WorkerScope.h" +#include "WorkerPrivate.h" + +// Bindings for ResolveConstructors +#include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IDBDatabaseBinding.h" +#include "mozilla/dom/IDBFactoryBinding.h" +#include "mozilla/dom/IDBIndexBinding.h" +#include "mozilla/dom/IDBKeyRangeBinding.h" +#include "mozilla/dom/IDBMutableFileBinding.h" +#include "mozilla/dom/IDBObjectStoreBinding.h" +#include "mozilla/dom/IDBOpenDBRequestBinding.h" +#include "mozilla/dom/IDBRequestBinding.h" +#include "mozilla/dom/IDBTransactionBinding.h" +#include "mozilla/dom/IDBVersionChangeEventBinding.h" + +#ifdef ENABLE_INTL_API +#include "nsCharSeparatedTokenizer.h" +#include "unicode/locid.h" +#endif + +#define IDB_STR "indexedDB" + +// The two possible values for the data argument when receiving the disk space +// observer notification. +#define LOW_DISK_SPACE_DATA_FULL "full" +#define LOW_DISK_SPACE_DATA_FREE "free" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +using namespace mozilla::dom::quota; +using namespace mozilla::dom::workers; +using namespace mozilla::ipc; + +class FileManagerInfo +{ +public: + already_AddRefed<FileManager> + GetFileManager(PersistenceType aPersistenceType, + const nsAString& aName) const; + + void + AddFileManager(FileManager* aFileManager); + + bool + HasFileManagers() const + { + AssertIsOnIOThread(); + + return !mPersistentStorageFileManagers.IsEmpty() || + !mTemporaryStorageFileManagers.IsEmpty() || + !mDefaultStorageFileManagers.IsEmpty(); + } + + void + InvalidateAllFileManagers() const; + + void + InvalidateAndRemoveFileManagers(PersistenceType aPersistenceType); + + void + InvalidateAndRemoveFileManager(PersistenceType aPersistenceType, + const nsAString& aName); + +private: + nsTArray<RefPtr<FileManager> >& + GetArray(PersistenceType aPersistenceType); + + const nsTArray<RefPtr<FileManager> >& + GetImmutableArray(PersistenceType aPersistenceType) const + { + return const_cast<FileManagerInfo*>(this)->GetArray(aPersistenceType); + } + + nsTArray<RefPtr<FileManager> > mPersistentStorageFileManagers; + nsTArray<RefPtr<FileManager> > mTemporaryStorageFileManagers; + nsTArray<RefPtr<FileManager> > mDefaultStorageFileManagers; +}; + +} // namespace indexedDB + +using namespace mozilla::dom::indexedDB; + +namespace { + +NS_DEFINE_IID(kIDBRequestIID, PRIVATE_IDBREQUEST_IID); + +const uint32_t kDeleteTimeoutMs = 1000; + +// The threshold we use for structured clone data storing. +// Anything smaller than the threshold is compressed and stored in the database. +// Anything larger is compressed and stored outside the database. +const int32_t kDefaultDataThresholdBytes = 1024 * 1024; // 1MB + +// The maximal size of a serialized object to be transfered through IPC. +const int32_t kDefaultMaxSerializedMsgSize = IPC::Channel::kMaximumMessageSize; + +#define IDB_PREF_BRANCH_ROOT "dom.indexedDB." + +const char kTestingPref[] = IDB_PREF_BRANCH_ROOT "testing"; +const char kPrefExperimental[] = IDB_PREF_BRANCH_ROOT "experimental"; +const char kPrefFileHandle[] = "dom.fileHandle.enabled"; +const char kDataThresholdPref[] = IDB_PREF_BRANCH_ROOT "dataThreshold"; +const char kPrefMaxSerilizedMsgSize[] = IDB_PREF_BRANCH_ROOT "maxSerializedMsgSize"; + +#define IDB_PREF_LOGGING_BRANCH_ROOT IDB_PREF_BRANCH_ROOT "logging." + +const char kPrefLoggingEnabled[] = IDB_PREF_LOGGING_BRANCH_ROOT "enabled"; +const char kPrefLoggingDetails[] = IDB_PREF_LOGGING_BRANCH_ROOT "details"; + +#if defined(DEBUG) || defined(MOZ_ENABLE_PROFILER_SPS) +const char kPrefLoggingProfiler[] = + IDB_PREF_LOGGING_BRANCH_ROOT "profiler-marks"; +#endif + +#undef IDB_PREF_LOGGING_BRANCH_ROOT +#undef IDB_PREF_BRANCH_ROOT + +StaticRefPtr<IndexedDatabaseManager> gDBManager; + +Atomic<bool> gInitialized(false); +Atomic<bool> gClosed(false); +Atomic<bool> gTestingMode(false); +Atomic<bool> gExperimentalFeaturesEnabled(false); +Atomic<bool> gFileHandleEnabled(false); +Atomic<int32_t> gDataThresholdBytes(0); +Atomic<int32_t> gMaxSerializedMsgSize(0); + +class DeleteFilesRunnable final + : public nsIRunnable + , public OpenDirectoryListener +{ + typedef mozilla::dom::quota::DirectoryLock DirectoryLock; + + enum State + { + // Just created on the main thread. Next step is State_DirectoryOpenPending. + State_Initial, + + // Waiting for directory open allowed on the main thread. The next step is + // State_DatabaseWorkOpen. + State_DirectoryOpenPending, + + // Waiting to do/doing work on the QuotaManager IO thread. The next step is + // State_UnblockingOpen. + State_DatabaseWorkOpen, + + // Notifying the QuotaManager that it can proceed to the next operation on + // the main thread. Next step is State_Completed. + State_UnblockingOpen, + + // All done. + State_Completed + }; + + nsCOMPtr<nsIEventTarget> mBackgroundThread; + + RefPtr<FileManager> mFileManager; + nsTArray<int64_t> mFileIds; + + RefPtr<DirectoryLock> mDirectoryLock; + + nsCOMPtr<nsIFile> mDirectory; + nsCOMPtr<nsIFile> mJournalDirectory; + + State mState; + +public: + DeleteFilesRunnable(nsIEventTarget* aBackgroundThread, + FileManager* aFileManager, + nsTArray<int64_t>& aFileIds); + + void + Dispatch(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + virtual void + DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void + DirectoryLockFailed() override; + +private: + ~DeleteFilesRunnable() {} + + nsresult + Open(); + + nsresult + DeleteFile(int64_t aFileId); + + nsresult + DoDatabaseWork(); + + void + Finish(); + + void + UnblockOpen(); +}; + +void +AtomicBoolPrefChangedCallback(const char* aPrefName, void* aClosure) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aClosure); + + *static_cast<Atomic<bool>*>(aClosure) = Preferences::GetBool(aPrefName); +} + +void +DataThresholdPrefChangedCallback(const char* aPrefName, void* aClosure) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kDataThresholdPref)); + MOZ_ASSERT(!aClosure); + + int32_t dataThresholdBytes = + Preferences::GetInt(aPrefName, kDefaultDataThresholdBytes); + + // The magic -1 is for use only by tests that depend on stable blob file id's. + if (dataThresholdBytes == -1) { + dataThresholdBytes = INT32_MAX; + } + + gDataThresholdBytes = dataThresholdBytes; +} + +void +MaxSerializedMsgSizePrefChangeCallback(const char* aPrefName, void* aClosure) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kPrefMaxSerilizedMsgSize)); + MOZ_ASSERT(!aClosure); + + gMaxSerializedMsgSize = + Preferences::GetInt(aPrefName, kDefaultMaxSerializedMsgSize); + MOZ_ASSERT(gMaxSerializedMsgSize > 0); +} + +} // namespace + +IndexedDatabaseManager::IndexedDatabaseManager() + : mFileMutex("IndexedDatabaseManager.mFileMutex") + , mBackgroundActor(nullptr) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); +} + +IndexedDatabaseManager::~IndexedDatabaseManager() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (mBackgroundActor) { + mBackgroundActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } +} + +bool IndexedDatabaseManager::sIsMainProcess = false; +bool IndexedDatabaseManager::sFullSynchronousMode = false; + +mozilla::LazyLogModule IndexedDatabaseManager::sLoggingModule("IndexedDB"); + +Atomic<IndexedDatabaseManager::LoggingMode> + IndexedDatabaseManager::sLoggingMode( + IndexedDatabaseManager::Logging_Disabled); + +mozilla::Atomic<bool> IndexedDatabaseManager::sLowDiskSpaceMode(false); + +// static +IndexedDatabaseManager* +IndexedDatabaseManager::GetOrCreate() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (IsClosed()) { + NS_ERROR("Calling GetOrCreate() after shutdown!"); + return nullptr; + } + + if (!gDBManager) { + sIsMainProcess = XRE_IsParentProcess(); + + if (sIsMainProcess && Preferences::GetBool("disk_space_watcher.enabled", false)) { + // See if we're starting up in low disk space conditions. + nsCOMPtr<nsIDiskSpaceWatcher> watcher = + do_GetService(DISKSPACEWATCHER_CONTRACTID); + if (watcher) { + bool isDiskFull; + if (NS_SUCCEEDED(watcher->GetIsDiskFull(&isDiskFull))) { + sLowDiskSpaceMode = isDiskFull; + } + else { + NS_WARNING("GetIsDiskFull failed!"); + } + } + else { + NS_WARNING("No disk space watcher component available!"); + } + } + + RefPtr<IndexedDatabaseManager> instance(new IndexedDatabaseManager()); + + nsresult rv = instance->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + + if (gInitialized.exchange(true)) { + NS_ERROR("Initialized more than once?!"); + } + + gDBManager = instance; + + ClearOnShutdown(&gDBManager); + } + + return gDBManager; +} + +// static +IndexedDatabaseManager* +IndexedDatabaseManager::Get() +{ + // Does not return an owning reference. + return gDBManager; +} + +nsresult +IndexedDatabaseManager::Init() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // During Init() we can't yet call IsMainProcess(), just check sIsMainProcess + // directly. + if (sIsMainProcess) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + NS_ENSURE_STATE(obs); + + nsresult rv = + obs->AddObserver(this, DISKSPACEWATCHER_OBSERVER_TOPIC, false); + NS_ENSURE_SUCCESS(rv, rv); + + mDeleteTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + NS_ENSURE_STATE(mDeleteTimer); + + if (QuotaManager* quotaManager = QuotaManager::Get()) { + NoteLiveQuotaManager(quotaManager); + } + } + + Preferences::RegisterCallbackAndCall(AtomicBoolPrefChangedCallback, + kTestingPref, + &gTestingMode); + Preferences::RegisterCallbackAndCall(AtomicBoolPrefChangedCallback, + kPrefExperimental, + &gExperimentalFeaturesEnabled); + Preferences::RegisterCallbackAndCall(AtomicBoolPrefChangedCallback, + kPrefFileHandle, + &gFileHandleEnabled); + + // By default IndexedDB uses SQLite with PRAGMA synchronous = NORMAL. This + // guarantees (unlike synchronous = OFF) atomicity and consistency, but not + // necessarily durability in situations such as power loss. This preference + // allows enabling PRAGMA synchronous = FULL on SQLite, which does guarantee + // durability, but with an extra fsync() and the corresponding performance + // hit. + sFullSynchronousMode = Preferences::GetBool("dom.indexedDB.fullSynchronous"); + + Preferences::RegisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingDetails); +#ifdef MOZ_ENABLE_PROFILER_SPS + Preferences::RegisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingProfiler); +#endif + Preferences::RegisterCallbackAndCall(LoggingModePrefChangedCallback, + kPrefLoggingEnabled); + + Preferences::RegisterCallbackAndCall(DataThresholdPrefChangedCallback, + kDataThresholdPref); + + Preferences::RegisterCallbackAndCall(MaxSerializedMsgSizePrefChangeCallback, + kPrefMaxSerilizedMsgSize); + +#ifdef ENABLE_INTL_API + const nsAdoptingCString& acceptLang = + Preferences::GetLocalizedCString("intl.accept_languages"); + + // Split values on commas. + nsCCharSeparatedTokenizer langTokenizer(acceptLang, ','); + while (langTokenizer.hasMoreTokens()) { + nsAutoCString lang(langTokenizer.nextToken()); + icu::Locale locale = icu::Locale::createCanonical(lang.get()); + if (!locale.isBogus()) { + // icu::Locale::getBaseName is always ASCII as per BCP 47 + mLocale.AssignASCII(locale.getBaseName()); + break; + } + } + + if (mLocale.IsEmpty()) { + mLocale.AssignLiteral("en_US"); + } +#endif + + return NS_OK; +} + +void +IndexedDatabaseManager::Destroy() +{ + // Setting the closed flag prevents the service from being recreated. + // Don't set it though if there's no real instance created. + if (gInitialized && gClosed.exchange(true)) { + NS_ERROR("Shutdown more than once?!"); + } + + if (sIsMainProcess && mDeleteTimer) { + if (NS_FAILED(mDeleteTimer->Cancel())) { + NS_WARNING("Failed to cancel timer!"); + } + + mDeleteTimer = nullptr; + } + + Preferences::UnregisterCallback(AtomicBoolPrefChangedCallback, + kTestingPref, + &gTestingMode); + Preferences::UnregisterCallback(AtomicBoolPrefChangedCallback, + kPrefExperimental, + &gExperimentalFeaturesEnabled); + Preferences::UnregisterCallback(AtomicBoolPrefChangedCallback, + kPrefFileHandle, + &gFileHandleEnabled); + + Preferences::UnregisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingDetails); +#ifdef MOZ_ENABLE_PROFILER_SPS + Preferences::UnregisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingProfiler); +#endif + Preferences::UnregisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingEnabled); + + Preferences::UnregisterCallback(DataThresholdPrefChangedCallback, + kDataThresholdPref); + + Preferences::UnregisterCallback(MaxSerializedMsgSizePrefChangeCallback, + kPrefMaxSerilizedMsgSize); + + delete this; +} + +// static +nsresult +IndexedDatabaseManager::CommonPostHandleEvent(EventChainPostVisitor& aVisitor, + IDBFactory* aFactory) +{ + MOZ_ASSERT(aVisitor.mDOMEvent); + MOZ_ASSERT(aFactory); + + if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { + return NS_OK; + } + + Event* internalEvent = aVisitor.mDOMEvent->InternalDOMEvent(); + MOZ_ASSERT(internalEvent); + + if (!internalEvent->IsTrusted()) { + return NS_OK; + } + + nsString type; + MOZ_ALWAYS_SUCCEEDS(internalEvent->GetType(type)); + + MOZ_ASSERT(nsDependentString(kErrorEventType).EqualsLiteral("error")); + if (!type.EqualsLiteral("error")) { + return NS_OK; + } + + nsCOMPtr<EventTarget> eventTarget = internalEvent->GetTarget(); + MOZ_ASSERT(eventTarget); + + // Only mess with events that were originally targeted to an IDBRequest. + RefPtr<IDBRequest> request; + if (NS_FAILED(eventTarget->QueryInterface(kIDBRequestIID, + getter_AddRefs(request))) || + !request) { + return NS_OK; + } + + RefPtr<DOMError> error = request->GetErrorAfterResult(); + + nsString errorName; + if (error) { + error->GetName(errorName); + } + + RootedDictionary<ErrorEventInit> init(RootingCx()); + request->GetCallerLocation(init.mFilename, &init.mLineno, &init.mColno); + + init.mMessage = errorName; + init.mCancelable = true; + init.mBubbles = true; + + nsEventStatus status = nsEventStatus_eIgnore; + + if (NS_IsMainThread()) { + nsCOMPtr<nsIDOMWindow> window = do_QueryInterface(eventTarget->GetOwnerGlobal()); + if (window) { + nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(window); + MOZ_ASSERT(sgo); + + if (NS_WARN_IF(NS_FAILED(sgo->HandleScriptError(init, &status)))) { + status = nsEventStatus_eIgnore; + } + } else { + // We don't fire error events at any global for non-window JS on the main + // thread. + } + } else { + // Not on the main thread, must be in a worker. + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<WorkerGlobalScope> globalScope = workerPrivate->GlobalScope(); + MOZ_ASSERT(globalScope); + + RefPtr<ErrorEvent> errorEvent = + ErrorEvent::Constructor(globalScope, + nsDependentString(kErrorEventType), + init); + MOZ_ASSERT(errorEvent); + + errorEvent->SetTrusted(true); + + auto* target = static_cast<EventTarget*>(globalScope.get()); + + if (NS_WARN_IF(NS_FAILED( + EventDispatcher::DispatchDOMEvent(target, + /* aWidgetEvent */ nullptr, + errorEvent, + /* aPresContext */ nullptr, + &status)))) { + status = nsEventStatus_eIgnore; + } + } + + if (status == nsEventStatus_eConsumeNoDefault) { + return NS_OK; + } + + // Log the error to the error console. + ScriptErrorHelper::Dump(errorName, + init.mFilename, + init.mLineno, + init.mColno, + nsIScriptError::errorFlag, + aFactory->IsChrome(), + aFactory->InnerWindowID()); + + return NS_OK; +} + +// static +bool +IndexedDatabaseManager::ResolveSandboxBinding(JSContext* aCx) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(js::GetObjectClass(JS::CurrentGlobalOrNull(aCx))->flags & + JSCLASS_DOM_GLOBAL, + "Passed object is not a global object!"); + + // We need to ensure that the manager has been created already here so that we + // load preferences that may control which properties are exposed. + if (NS_WARN_IF(!GetOrCreate())) { + return false; + } + + if (!IDBCursorBinding::GetConstructorObject(aCx) || + !IDBCursorWithValueBinding::GetConstructorObject(aCx) || + !IDBDatabaseBinding::GetConstructorObject(aCx) || + !IDBFactoryBinding::GetConstructorObject(aCx) || + !IDBIndexBinding::GetConstructorObject(aCx) || + !IDBKeyRangeBinding::GetConstructorObject(aCx) || + !IDBLocaleAwareKeyRangeBinding::GetConstructorObject(aCx) || + !IDBMutableFileBinding::GetConstructorObject(aCx) || + !IDBObjectStoreBinding::GetConstructorObject(aCx) || + !IDBOpenDBRequestBinding::GetConstructorObject(aCx) || + !IDBRequestBinding::GetConstructorObject(aCx) || + !IDBTransactionBinding::GetConstructorObject(aCx) || + !IDBVersionChangeEventBinding::GetConstructorObject(aCx)) + { + return false; + } + + return true; +} + +// static +bool +IndexedDatabaseManager::DefineIndexedDB(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(js::GetObjectClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL, + "Passed object is not a global object!"); + + RefPtr<IDBFactory> factory; + if (NS_FAILED(IDBFactory::CreateForMainThreadJS(aCx, + aGlobal, + getter_AddRefs(factory)))) { + return false; + } + + MOZ_ASSERT(factory, "This should never fail for chrome!"); + + JS::Rooted<JS::Value> indexedDB(aCx); + js::AssertSameCompartment(aCx, aGlobal); + if (!GetOrCreateDOMReflector(aCx, factory, &indexedDB)) { + return false; + } + + return JS_DefineProperty(aCx, aGlobal, IDB_STR, indexedDB, JSPROP_ENUMERATE); +} + +// static +bool +IndexedDatabaseManager::IsClosed() +{ + return gClosed; +} + +#ifdef DEBUG +// static +bool +IndexedDatabaseManager::IsMainProcess() +{ + NS_ASSERTION(gDBManager, + "IsMainProcess() called before indexedDB has been initialized!"); + NS_ASSERTION((XRE_IsParentProcess()) == + sIsMainProcess, "XRE_GetProcessType changed its tune!"); + return sIsMainProcess; +} + +//static +bool +IndexedDatabaseManager::InLowDiskSpaceMode() +{ + NS_ASSERTION(gDBManager, + "InLowDiskSpaceMode() called before indexedDB has been " + "initialized!"); + return sLowDiskSpaceMode; +} + +// static +IndexedDatabaseManager::LoggingMode +IndexedDatabaseManager::GetLoggingMode() +{ + MOZ_ASSERT(gDBManager, + "GetLoggingMode called before IndexedDatabaseManager has been " + "initialized!"); + + return sLoggingMode; +} + +// static +mozilla::LogModule* +IndexedDatabaseManager::GetLoggingModule() +{ + MOZ_ASSERT(gDBManager, + "GetLoggingModule called before IndexedDatabaseManager has been " + "initialized!"); + + return sLoggingModule; +} + +#endif // DEBUG + +// static +bool +IndexedDatabaseManager::InTestingMode() +{ + MOZ_ASSERT(gDBManager, + "InTestingMode() called before indexedDB has been initialized!"); + + return gTestingMode; +} + +// static +bool +IndexedDatabaseManager::FullSynchronous() +{ + MOZ_ASSERT(gDBManager, + "FullSynchronous() called before indexedDB has been initialized!"); + + return sFullSynchronousMode; +} + +// static +bool +IndexedDatabaseManager::ExperimentalFeaturesEnabled() +{ + if (NS_IsMainThread()) { + if (NS_WARN_IF(!GetOrCreate())) { + return false; + } + } else { + MOZ_ASSERT(Get(), + "ExperimentalFeaturesEnabled() called off the main thread " + "before indexedDB has been initialized!"); + } + + return gExperimentalFeaturesEnabled; +} + +// static +bool +IndexedDatabaseManager::ExperimentalFeaturesEnabled(JSContext* aCx, JSObject* aGlobal) +{ + // If, in the child process, properties of the global object are enumerated + // before the chrome registry (and thus the value of |intl.accept_languages|) + // is ready, calling IndexedDatabaseManager::Init will permanently break + // that preference. We can retrieve gExperimentalFeaturesEnabled without + // actually going through IndexedDatabaseManager. + // See Bug 1198093 comment 14 for detailed explanation. + if (IsNonExposedGlobal(aCx, js::GetGlobalForObjectCrossCompartment(aGlobal), + GlobalNames::BackstagePass)) { + MOZ_ASSERT(NS_IsMainThread()); + static bool featureRetrieved = false; + if (!featureRetrieved) { + gExperimentalFeaturesEnabled = Preferences::GetBool(kPrefExperimental); + featureRetrieved = true; + } + return gExperimentalFeaturesEnabled; + } + + return ExperimentalFeaturesEnabled(); +} + +// static +bool +IndexedDatabaseManager::IsFileHandleEnabled() +{ + MOZ_ASSERT(gDBManager, + "IsFileHandleEnabled() called before indexedDB has been " + "initialized!"); + + return gFileHandleEnabled; +} + +// static +uint32_t +IndexedDatabaseManager::DataThreshold() +{ + MOZ_ASSERT(gDBManager, + "DataThreshold() called before indexedDB has been initialized!"); + + return gDataThresholdBytes; +} + +// static +uint32_t +IndexedDatabaseManager::MaxSerializedMsgSize() +{ + MOZ_ASSERT(gDBManager, + "MaxSerializedMsgSize() called before indexedDB has been initialized!"); + MOZ_ASSERT(gMaxSerializedMsgSize > 0); + + return gMaxSerializedMsgSize; +} + +void +IndexedDatabaseManager::ClearBackgroundActor() +{ + MOZ_ASSERT(NS_IsMainThread()); + + mBackgroundActor = nullptr; +} + +void +IndexedDatabaseManager::NoteLiveQuotaManager(QuotaManager* aQuotaManager) +{ + MOZ_ASSERT(IsMainProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aQuotaManager); + + mBackgroundThread = aQuotaManager->OwningThread(); +} + +void +IndexedDatabaseManager::NoteShuttingDownQuotaManager() +{ + MOZ_ASSERT(IsMainProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel()); + + mBackgroundThread = nullptr; +} + +already_AddRefed<FileManager> +IndexedDatabaseManager::GetFileManager(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName) +{ + AssertIsOnIOThread(); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aOrigin, &info)) { + return nullptr; + } + + RefPtr<FileManager> fileManager = + info->GetFileManager(aPersistenceType, aDatabaseName); + + return fileManager.forget(); +} + +void +IndexedDatabaseManager::AddFileManager(FileManager* aFileManager) +{ + AssertIsOnIOThread(); + NS_ASSERTION(aFileManager, "Null file manager!"); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aFileManager->Origin(), &info)) { + info = new FileManagerInfo(); + mFileManagerInfos.Put(aFileManager->Origin(), info); + } + + info->AddFileManager(aFileManager); +} + +void +IndexedDatabaseManager::InvalidateAllFileManagers() +{ + AssertIsOnIOThread(); + + for (auto iter = mFileManagerInfos.ConstIter(); !iter.Done(); iter.Next()) { + auto value = iter.Data(); + MOZ_ASSERT(value); + + value->InvalidateAllFileManagers(); + } + + mFileManagerInfos.Clear(); +} + +void +IndexedDatabaseManager::InvalidateFileManagers(PersistenceType aPersistenceType, + const nsACString& aOrigin) +{ + AssertIsOnIOThread(); + MOZ_ASSERT(!aOrigin.IsEmpty()); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aOrigin, &info)) { + return; + } + + info->InvalidateAndRemoveFileManagers(aPersistenceType); + + if (!info->HasFileManagers()) { + mFileManagerInfos.Remove(aOrigin); + } +} + +void +IndexedDatabaseManager::InvalidateFileManager(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName) +{ + AssertIsOnIOThread(); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aOrigin, &info)) { + return; + } + + info->InvalidateAndRemoveFileManager(aPersistenceType, aDatabaseName); + + if (!info->HasFileManagers()) { + mFileManagerInfos.Remove(aOrigin); + } +} + +nsresult +IndexedDatabaseManager::AsyncDeleteFile(FileManager* aFileManager, + int64_t aFileId) +{ + MOZ_ASSERT(IsMainProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFileManager); + MOZ_ASSERT(aFileId > 0); + MOZ_ASSERT(mDeleteTimer); + + if (!mBackgroundThread) { + return NS_OK; + } + + nsresult rv = mDeleteTimer->Cancel(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mDeleteTimer->InitWithCallback(this, kDeleteTimeoutMs, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsTArray<int64_t>* array; + if (!mPendingDeleteInfos.Get(aFileManager, &array)) { + array = new nsTArray<int64_t>(); + mPendingDeleteInfos.Put(aFileManager, array); + } + + array->AppendElement(aFileId); + + return NS_OK; +} + +nsresult +IndexedDatabaseManager::BlockAndGetFileReferences( + PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName, + int64_t aFileId, + int32_t* aRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt, + bool* aResult) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!InTestingMode())) { + return NS_ERROR_UNEXPECTED; + } + + if (!mBackgroundActor) { + PBackgroundChild* bgActor = BackgroundChild::GetForCurrentThread(); + if (NS_WARN_IF(!bgActor)) { + return NS_ERROR_FAILURE; + } + + BackgroundUtilsChild* actor = new BackgroundUtilsChild(this); + + mBackgroundActor = + static_cast<BackgroundUtilsChild*>( + bgActor->SendPBackgroundIndexedDBUtilsConstructor(actor)); + } + + if (NS_WARN_IF(!mBackgroundActor)) { + return NS_ERROR_FAILURE; + } + + if (!mBackgroundActor->SendGetFileReferences(aPersistenceType, + nsCString(aOrigin), + nsString(aDatabaseName), + aFileId, + aRefCnt, + aDBRefCnt, + aSliceRefCnt, + aResult)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +IndexedDatabaseManager::FlushPendingFileDeletions() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!InTestingMode())) { + return NS_ERROR_UNEXPECTED; + } + + if (IsMainProcess()) { + nsresult rv = mDeleteTimer->Cancel(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Notify(mDeleteTimer); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + PBackgroundChild* bgActor = BackgroundChild::GetForCurrentThread(); + if (NS_WARN_IF(!bgActor)) { + return NS_ERROR_FAILURE; + } + + if (!bgActor->SendFlushPendingFileDeletions()) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +// static +void +IndexedDatabaseManager::LoggingModePrefChangedCallback( + const char* /* aPrefName */, + void* /* aClosure */) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!Preferences::GetBool(kPrefLoggingEnabled)) { + sLoggingMode = Logging_Disabled; + return; + } + + bool useProfiler = +#if defined(DEBUG) || defined(MOZ_ENABLE_PROFILER_SPS) + Preferences::GetBool(kPrefLoggingProfiler); +#if !defined(MOZ_ENABLE_PROFILER_SPS) + if (useProfiler) { + NS_WARNING("IndexedDB cannot create profiler marks because this build does " + "not have profiler extensions enabled!"); + useProfiler = false; + } +#endif +#else + false; +#endif + + const bool logDetails = Preferences::GetBool(kPrefLoggingDetails); + + if (useProfiler) { + sLoggingMode = logDetails ? + Logging_DetailedProfilerMarks : + Logging_ConciseProfilerMarks; + } else { + sLoggingMode = logDetails ? Logging_Detailed : Logging_Concise; + } +} + +#ifdef ENABLE_INTL_API +// static +const nsCString& +IndexedDatabaseManager::GetLocale() +{ + IndexedDatabaseManager* idbManager = Get(); + MOZ_ASSERT(idbManager, "IDBManager is not ready!"); + + return idbManager->mLocale; +} +#endif + +NS_IMPL_ADDREF(IndexedDatabaseManager) +NS_IMPL_RELEASE_WITH_DESTROY(IndexedDatabaseManager, Destroy()) +NS_IMPL_QUERY_INTERFACE(IndexedDatabaseManager, nsIObserver, nsITimerCallback) + +NS_IMETHODIMP +IndexedDatabaseManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + NS_ASSERTION(IsMainProcess(), "Wrong process!"); + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (!strcmp(aTopic, DISKSPACEWATCHER_OBSERVER_TOPIC)) { + NS_ASSERTION(aData, "No data?!"); + + const nsDependentString data(aData); + + if (data.EqualsLiteral(LOW_DISK_SPACE_DATA_FULL)) { + sLowDiskSpaceMode = true; + } + else if (data.EqualsLiteral(LOW_DISK_SPACE_DATA_FREE)) { + sLowDiskSpaceMode = false; + } + else { + NS_NOTREACHED("Unknown data value!"); + } + + return NS_OK; + } + + NS_NOTREACHED("Unknown topic!"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +IndexedDatabaseManager::Notify(nsITimer* aTimer) +{ + MOZ_ASSERT(IsMainProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mBackgroundThread); + + for (auto iter = mPendingDeleteInfos.ConstIter(); !iter.Done(); iter.Next()) { + auto key = iter.Key(); + auto value = iter.Data(); + MOZ_ASSERT(!value->IsEmpty()); + + RefPtr<DeleteFilesRunnable> runnable = + new DeleteFilesRunnable(mBackgroundThread, key, *value); + + MOZ_ASSERT(value->IsEmpty()); + + runnable->Dispatch(); + } + + mPendingDeleteInfos.Clear(); + + return NS_OK; +} + +already_AddRefed<FileManager> +FileManagerInfo::GetFileManager(PersistenceType aPersistenceType, + const nsAString& aName) const +{ + AssertIsOnIOThread(); + + const nsTArray<RefPtr<FileManager> >& managers = + GetImmutableArray(aPersistenceType); + + for (uint32_t i = 0; i < managers.Length(); i++) { + const RefPtr<FileManager>& fileManager = managers[i]; + + if (fileManager->DatabaseName() == aName) { + RefPtr<FileManager> result = fileManager; + return result.forget(); + } + } + + return nullptr; +} + +void +FileManagerInfo::AddFileManager(FileManager* aFileManager) +{ + AssertIsOnIOThread(); + + nsTArray<RefPtr<FileManager> >& managers = GetArray(aFileManager->Type()); + + NS_ASSERTION(!managers.Contains(aFileManager), "Adding more than once?!"); + + managers.AppendElement(aFileManager); +} + +void +FileManagerInfo::InvalidateAllFileManagers() const +{ + AssertIsOnIOThread(); + + uint32_t i; + + for (i = 0; i < mPersistentStorageFileManagers.Length(); i++) { + mPersistentStorageFileManagers[i]->Invalidate(); + } + + for (i = 0; i < mTemporaryStorageFileManagers.Length(); i++) { + mTemporaryStorageFileManagers[i]->Invalidate(); + } + + for (i = 0; i < mDefaultStorageFileManagers.Length(); i++) { + mDefaultStorageFileManagers[i]->Invalidate(); + } +} + +void +FileManagerInfo::InvalidateAndRemoveFileManagers( + PersistenceType aPersistenceType) +{ + AssertIsOnIOThread(); + + nsTArray<RefPtr<FileManager > >& managers = GetArray(aPersistenceType); + + for (uint32_t i = 0; i < managers.Length(); i++) { + managers[i]->Invalidate(); + } + + managers.Clear(); +} + +void +FileManagerInfo::InvalidateAndRemoveFileManager( + PersistenceType aPersistenceType, + const nsAString& aName) +{ + AssertIsOnIOThread(); + + nsTArray<RefPtr<FileManager > >& managers = GetArray(aPersistenceType); + + for (uint32_t i = 0; i < managers.Length(); i++) { + RefPtr<FileManager>& fileManager = managers[i]; + if (fileManager->DatabaseName() == aName) { + fileManager->Invalidate(); + managers.RemoveElementAt(i); + return; + } + } +} + +nsTArray<RefPtr<FileManager> >& +FileManagerInfo::GetArray(PersistenceType aPersistenceType) +{ + switch (aPersistenceType) { + case PERSISTENCE_TYPE_PERSISTENT: + return mPersistentStorageFileManagers; + case PERSISTENCE_TYPE_TEMPORARY: + return mTemporaryStorageFileManagers; + case PERSISTENCE_TYPE_DEFAULT: + return mDefaultStorageFileManagers; + + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad storage type value!"); + } +} + +DeleteFilesRunnable::DeleteFilesRunnable(nsIEventTarget* aBackgroundThread, + FileManager* aFileManager, + nsTArray<int64_t>& aFileIds) + : mBackgroundThread(aBackgroundThread) + , mFileManager(aFileManager) + , mState(State_Initial) +{ + mFileIds.SwapElements(aFileIds); +} + +void +DeleteFilesRunnable::Dispatch() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State_Initial); + + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +NS_IMPL_ISUPPORTS(DeleteFilesRunnable, nsIRunnable) + +NS_IMETHODIMP +DeleteFilesRunnable::Run() +{ + nsresult rv; + + switch (mState) { + case State_Initial: + rv = Open(); + break; + + case State_DatabaseWorkOpen: + rv = DoDatabaseWork(); + break; + + case State_UnblockingOpen: + UnblockOpen(); + return NS_OK; + + case State_DirectoryOpenPending: + default: + MOZ_CRASH("Should never get here!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State_UnblockingOpen) { + Finish(); + } + + return NS_OK; +} + +void +DeleteFilesRunnable::DirectoryLockAcquired(DirectoryLock* aLock) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO thread + mState = State_DatabaseWorkOpen; + + nsresult rv = quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(); + return; + } +} + +void +DeleteFilesRunnable::DirectoryLockFailed() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + Finish(); +} + +nsresult +DeleteFilesRunnable::Open() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_Initial); + + QuotaManager* quotaManager = QuotaManager::Get(); + if (NS_WARN_IF(!quotaManager)) { + return NS_ERROR_FAILURE; + } + + mState = State_DirectoryOpenPending; + + quotaManager->OpenDirectory(mFileManager->Type(), + mFileManager->Group(), + mFileManager->Origin(), + mFileManager->IsApp(), + Client::IDB, + /* aExclusive */ false, + this); + + return NS_OK; +} + +nsresult +DeleteFilesRunnable::DeleteFile(int64_t aFileId) +{ + MOZ_ASSERT(mDirectory); + MOZ_ASSERT(mJournalDirectory); + + nsCOMPtr<nsIFile> file = mFileManager->GetFileForId(mDirectory, aFileId); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + nsresult rv; + int64_t fileSize; + + if (mFileManager->EnforcingQuota()) { + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + } + + rv = file->Remove(false); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + if (mFileManager->EnforcingQuota()) { + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Shouldn't be null!"); + + quotaManager->DecreaseUsageForOrigin(mFileManager->Type(), + mFileManager->Group(), + mFileManager->Origin(), fileSize); + } + + file = mFileManager->GetFileForId(mJournalDirectory, aFileId); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + rv = file->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +DeleteFilesRunnable::DoDatabaseWork() +{ + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State_DatabaseWorkOpen); + + if (!mFileManager->Invalidated()) { + mDirectory = mFileManager->GetDirectory(); + if (NS_WARN_IF(!mDirectory)) { + return NS_ERROR_FAILURE; + } + + mJournalDirectory = mFileManager->GetJournalDirectory(); + if (NS_WARN_IF(!mJournalDirectory)) { + return NS_ERROR_FAILURE; + } + + for (int64_t fileId : mFileIds) { + if (NS_FAILED(DeleteFile(fileId))) { + NS_WARNING("Failed to delete file!"); + } + } + } + + Finish(); + + return NS_OK; +} + +void +DeleteFilesRunnable::Finish() +{ + // Must set mState before dispatching otherwise we will race with the main + // thread. + mState = State_UnblockingOpen; + + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void +DeleteFilesRunnable::UnblockOpen() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_UnblockingOpen); + + mDirectoryLock = nullptr; + + mState = State_Completed; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/IndexedDatabaseManager.h b/dom/indexedDB/IndexedDatabaseManager.h new file mode 100644 index 000000000..8bb9d7003 --- /dev/null +++ b/dom/indexedDB/IndexedDatabaseManager.h @@ -0,0 +1,257 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddatabasemanager_h__ +#define mozilla_dom_indexeddatabasemanager_h__ + +#include "nsIObserver.h" + +#include "js/TypeDecls.h" +#include "mozilla/Atomics.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/Mutex.h" +#include "nsClassHashtable.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsITimer.h" + +class nsIEventTarget; + +namespace mozilla { + +class EventChainPostVisitor; + +namespace dom { + +class IDBFactory; + +namespace quota { + +class QuotaManager; + +} // namespace quota + +namespace indexedDB { + +class BackgroundUtilsChild; +class FileManager; +class FileManagerInfo; + +} // namespace indexedDB + +class IndexedDatabaseManager final + : public nsIObserver + , public nsITimerCallback +{ + typedef mozilla::dom::quota::PersistenceType PersistenceType; + typedef mozilla::dom::quota::QuotaManager QuotaManager; + typedef mozilla::dom::indexedDB::FileManager FileManager; + typedef mozilla::dom::indexedDB::FileManagerInfo FileManagerInfo; + +public: + enum LoggingMode + { + Logging_Disabled = 0, + Logging_Concise, + Logging_Detailed, + Logging_ConciseProfilerMarks, + Logging_DetailedProfilerMarks + }; + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + + // Returns a non-owning reference. + static IndexedDatabaseManager* + GetOrCreate(); + + // Returns a non-owning reference. + static IndexedDatabaseManager* + Get(); + + static bool + IsClosed(); + + static bool + IsMainProcess() +#ifdef DEBUG + ; +#else + { + return sIsMainProcess; + } +#endif + + static bool + InLowDiskSpaceMode() +#ifdef DEBUG + ; +#else + { + return !!sLowDiskSpaceMode; + } +#endif + + static bool + InTestingMode(); + + static bool + FullSynchronous(); + + static LoggingMode + GetLoggingMode() +#ifdef DEBUG + ; +#else + { + return sLoggingMode; + } +#endif + + static mozilla::LogModule* + GetLoggingModule() +#ifdef DEBUG + ; +#else + { + return sLoggingModule; + } +#endif + + static bool + ExperimentalFeaturesEnabled(); + + static bool + ExperimentalFeaturesEnabled(JSContext* aCx, JSObject* aGlobal); + + static bool + IsFileHandleEnabled(); + + static uint32_t + DataThreshold(); + + static uint32_t + MaxSerializedMsgSize(); + + void + ClearBackgroundActor(); + + void + NoteLiveQuotaManager(QuotaManager* aQuotaManager); + + void + NoteShuttingDownQuotaManager(); + + already_AddRefed<FileManager> + GetFileManager(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName); + + void + AddFileManager(FileManager* aFileManager); + + void + InvalidateAllFileManagers(); + + void + InvalidateFileManagers(PersistenceType aPersistenceType, + const nsACString& aOrigin); + + void + InvalidateFileManager(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName); + + nsresult + AsyncDeleteFile(FileManager* aFileManager, + int64_t aFileId); + + // Don't call this method in real code, it blocks the main thread! + // It is intended to be used by mochitests to test correctness of the special + // reference counting of stored blobs/files. + nsresult + BlockAndGetFileReferences(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName, + int64_t aFileId, + int32_t* aRefCnt, + int32_t* aDBRefCnt, + int32_t* aSliceRefCnt, + bool* aResult); + + nsresult + FlushPendingFileDeletions(); + +#ifdef ENABLE_INTL_API + static const nsCString& + GetLocale(); +#endif + + static mozilla::Mutex& + FileMutex() + { + IndexedDatabaseManager* mgr = Get(); + NS_ASSERTION(mgr, "Must have a manager here!"); + + return mgr->mFileMutex; + } + + static nsresult + CommonPostHandleEvent(EventChainPostVisitor& aVisitor, IDBFactory* aFactory); + + static bool + ResolveSandboxBinding(JSContext* aCx); + + static bool + DefineIndexedDB(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + +private: + IndexedDatabaseManager(); + ~IndexedDatabaseManager(); + + nsresult + Init(); + + void + Destroy(); + + static void + LoggingModePrefChangedCallback(const char* aPrefName, void* aClosure); + + nsCOMPtr<nsIEventTarget> mBackgroundThread; + + nsCOMPtr<nsITimer> mDeleteTimer; + + // Maintains a list of all file managers per origin. This list isn't + // protected by any mutex but it is only ever touched on the IO thread. + nsClassHashtable<nsCStringHashKey, FileManagerInfo> mFileManagerInfos; + + nsClassHashtable<nsRefPtrHashKey<FileManager>, + nsTArray<int64_t>> mPendingDeleteInfos; + + // Lock protecting FileManager.mFileInfos. + // It's s also used to atomically update FileInfo.mRefCnt, FileInfo.mDBRefCnt + // and FileInfo.mSliceRefCnt + mozilla::Mutex mFileMutex; + +#ifdef ENABLE_INTL_API + nsCString mLocale; +#endif + + indexedDB::BackgroundUtilsChild* mBackgroundActor; + + static bool sIsMainProcess; + static bool sFullSynchronousMode; + static LazyLogModule sLoggingModule; + static Atomic<LoggingMode> sLoggingMode; + static mozilla::Atomic<bool> sLowDiskSpaceMode; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddatabasemanager_h__ diff --git a/dom/indexedDB/Key.cpp b/dom/indexedDB/Key.cpp new file mode 100644 index 000000000..945320dd5 --- /dev/null +++ b/dom/indexedDB/Key.cpp @@ -0,0 +1,842 @@ +/* -*- 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 "Key.h" + +#include <algorithm> +#include "IndexedDatabaseManager.h" +#include "js/Date.h" +#include "js/Value.h" +#include "jsfriendapi.h" +#include "mozilla/Casting.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/FloatingPoint.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "nsAlgorithm.h" +#include "nsJSUtils.h" +#include "ReportInternalError.h" +#include "xpcpublic.h" + +#ifdef ENABLE_INTL_API +#include "unicode/ucol.h" +#endif + +namespace mozilla { +namespace dom { +namespace indexedDB { + +/* + Here's how we encode keys: + + Basic strategy is the following + + Numbers: 0x10 n n n n n n n n ("n"s are encoded 64bit float) + Dates: 0x20 n n n n n n n n ("n"s are encoded 64bit float) + Strings: 0x30 s s s ... 0 ("s"s are encoded unicode bytes) + Binaries: 0x40 s s s ... 0 ("s"s are encoded unicode bytes) + Arrays: 0x50 i i i ... 0 ("i"s are encoded array items) + + + When encoding floats, 64bit IEEE 754 are almost sortable, except that + positive sort lower than negative, and negative sort descending. So we use + the following encoding: + + value < 0 ? + (-to64bitInt(value)) : + (to64bitInt(value) | 0x8000000000000000) + + + When encoding strings, we use variable-size encoding per the following table + + Chars 0 - 7E are encoded as 0xxxxxxx with 1 added + Chars 7F - (3FFF+7F) are encoded as 10xxxxxx xxxxxxxx with 7F subtracted + Chars (3FFF+80) - FFFF are encoded as 11xxxxxx xxxxxxxx xx000000 + + This ensures that the first byte is never encoded as 0, which means that the + string terminator (per basic-stategy table) sorts before any character. + The reason that (3FFF+80) - FFFF is encoded "shifted up" 6 bits is to maximize + the chance that the last character is 0. See below for why. + + When encoding binaries, the algorithm is the same to how strings are encoded. + Since each octet in binary is in the range of [0-255], it'll take 1 to 2 encoded + unicode bytes. + + When encoding Arrays, we use an additional trick. Rather than adding a byte + containing the value 0x50 to indicate type, we instead add 0x50 to the next byte. + This is usually the byte containing the type of the first item in the array. + So simple examples are + + ["foo"] 0x80 s s s 0 0 // 0x80 is 0x30 + 0x50 + [1, 2] 0x60 n n n n n n n n 1 n n n n n n n n 0 // 0x60 is 0x10 + 0x50 + + Whe do this iteratively if the first item in the array is also an array + + [["foo"]] 0xA0 s s s 0 0 0 + + However, to avoid overflow in the byte, we only do this 3 times. If the first + item in an array is an array, and that array also has an array as first item, + we simply write out the total value accumulated so far and then follow the + "normal" rules. + + [[["foo"]]] 0xF0 0x30 s s s 0 0 0 0 + + There is another edge case that can happen though, which is that the array + doesn't have a first item to which we can add 0x50 to the type. Instead the + next byte would normally be the array terminator (per basic-strategy table) + so we simply add the 0x50 there. + + [[]] 0xA0 0 // 0xA0 is 0x50 + 0x50 + 0 + [] 0x50 // 0x50 is 0x50 + 0 + [[], "foo"] 0xA0 0x30 s s s 0 0 // 0xA0 is 0x50 + 0x50 + 0 + + Note that the max-3-times rule kicks in before we get a chance to add to the + array terminator + + [[[]]] 0xF0 0 0 0 // 0xF0 is 0x50 + 0x50 + 0x50 + + As a final optimization we do a post-encoding step which drops all 0s at the + end of the encoded buffer. + + "foo" // 0x30 s s s + 1 // 0x10 bf f0 + ["a", "b"] // 0x80 s 0 0x30 s + [1, 2] // 0x60 bf f0 0 0 0 0 0 0 0x10 c0 + [[]] // 0x80 +*/ +#ifdef ENABLE_INTL_API +nsresult +Key::ToLocaleBasedKey(Key& aTarget, const nsCString& aLocale) const +{ + if (IsUnset()) { + aTarget.Unset(); + return NS_OK; + } + + if (IsFloat() || IsDate() || IsBinary()) { + aTarget.mBuffer = mBuffer; + return NS_OK; + } + + aTarget.mBuffer.Truncate(); + aTarget.mBuffer.SetCapacity(mBuffer.Length()); + + auto* it = reinterpret_cast<const unsigned char*>(mBuffer.BeginReading()); + auto* end = reinterpret_cast<const unsigned char*>(mBuffer.EndReading()); + + // First we do a pass and see if there are any strings in this key. We only + // want to copy/decode when necessary. + bool canShareBuffers = true; + while (it < end) { + auto type = *it % eMaxType; + if (type == eTerminator || type == eArray) { + it++; + } else if (type == eFloat || type == eDate) { + it++; + it += std::min(sizeof(uint64_t), size_t(end - it)); + } else { + // We have a string! + canShareBuffers = false; + break; + } + } + + if (canShareBuffers) { + MOZ_ASSERT(it == end); + aTarget.mBuffer = mBuffer; + return NS_OK; + } + + // A string was found, so we need to copy the data we've read so far + auto* start = reinterpret_cast<const unsigned char*>(mBuffer.BeginReading()); + if (it > start) { + char* buffer; + if (!aTarget.mBuffer.GetMutableData(&buffer, it-start)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + while (start < it) { + *(buffer++) = *(start++); + } + } + + // Now continue decoding + while (it < end) { + char* buffer; + uint32_t oldLen = aTarget.mBuffer.Length(); + auto type = *it % eMaxType; + + if (type == eTerminator || type == eArray) { + // Copy array TypeID and terminator from raw key + if (!aTarget.mBuffer.GetMutableData(&buffer, oldLen + 1)) { + return NS_ERROR_OUT_OF_MEMORY; + } + *(buffer + oldLen) = *(it++); + } else if (type == eFloat || type == eDate) { + // Copy number from raw key + if (!aTarget.mBuffer.GetMutableData(&buffer, + oldLen + 1 + sizeof(uint64_t))) { + return NS_ERROR_OUT_OF_MEMORY; + } + buffer += oldLen; + *(buffer++) = *(it++); + + const size_t byteCount = std::min(sizeof(uint64_t), size_t(end - it)); + for (size_t count = 0; count < byteCount; count++) { + *(buffer++) = (*it++); + } + } else { + // Decode string and reencode + uint8_t typeOffset = *it - eString; + MOZ_ASSERT((typeOffset % eArray == 0) && (typeOffset / eArray <= 2)); + + nsDependentString str; + DecodeString(it, end, str); + aTarget.EncodeLocaleString(str, typeOffset, aLocale); + } + } + aTarget.TrimBuffer(); + return NS_OK; +} +#endif + +nsresult +Key::EncodeJSValInternal(JSContext* aCx, JS::Handle<JS::Value> aVal, + uint8_t aTypeOffset, uint16_t aRecursionDepth) +{ + static_assert(eMaxType * kMaxArrayCollapse < 256, + "Unable to encode jsvals."); + + if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + if (aVal.isString()) { + nsAutoJSString str; + if (!str.init(aCx, aVal)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + EncodeString(str, aTypeOffset); + return NS_OK; + } + + if (aVal.isNumber()) { + double d = aVal.toNumber(); + if (mozilla::IsNaN(d)) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + EncodeNumber(d, eFloat + aTypeOffset); + return NS_OK; + } + + if (aVal.isObject()) { + JS::Rooted<JSObject*> obj(aCx, &aVal.toObject()); + + js::ESClass cls; + if (!js::GetBuiltinClass(aCx, obj, &cls)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + if (cls == js::ESClass::Array) { + aTypeOffset += eMaxType; + + if (aTypeOffset == eMaxType * kMaxArrayCollapse) { + mBuffer.Append(aTypeOffset); + aTypeOffset = 0; + } + NS_ASSERTION((aTypeOffset % eMaxType) == 0 && + aTypeOffset < (eMaxType * kMaxArrayCollapse), + "Wrong typeoffset"); + + uint32_t length; + if (!JS_GetArrayLength(aCx, obj, &length)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + for (uint32_t index = 0; index < length; index++) { + JS::Rooted<JS::Value> val(aCx); + if (!JS_GetElement(aCx, obj, index, &val)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsresult rv = EncodeJSValInternal(aCx, val, aTypeOffset, + aRecursionDepth + 1); + if (NS_FAILED(rv)) { + return rv; + } + + aTypeOffset = 0; + } + + mBuffer.Append(eTerminator + aTypeOffset); + + return NS_OK; + } + + if (cls == js::ESClass::Date) { + bool valid; + if (!js::DateIsValid(aCx, obj, &valid)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + if (!valid) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + double t; + if (!js::DateGetMsecSinceEpoch(aCx, obj, &t)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + EncodeNumber(t, eDate + aTypeOffset); + return NS_OK; + } + + if (JS_IsArrayBufferObject(obj)) { + EncodeBinary(obj, /* aIsViewObject */ false, aTypeOffset); + return NS_OK; + } + + if (JS_IsArrayBufferViewObject(obj)) { + EncodeBinary(obj, /* aIsViewObject */ true, aTypeOffset); + return NS_OK; + } + } + + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; +} + +// static +nsresult +Key::DecodeJSValInternal(const unsigned char*& aPos, const unsigned char* aEnd, + JSContext* aCx, uint8_t aTypeOffset, JS::MutableHandle<JS::Value> aVal, + uint16_t aRecursionDepth) +{ + if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + if (*aPos - aTypeOffset >= eArray) { + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, 0)); + if (!array) { + NS_WARNING("Failed to make array!"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + aTypeOffset += eMaxType; + + if (aTypeOffset == eMaxType * kMaxArrayCollapse) { + ++aPos; + aTypeOffset = 0; + } + + uint32_t index = 0; + JS::Rooted<JS::Value> val(aCx); + while (aPos < aEnd && *aPos - aTypeOffset != eTerminator) { + nsresult rv = DecodeJSValInternal(aPos, aEnd, aCx, aTypeOffset, + &val, aRecursionDepth + 1); + NS_ENSURE_SUCCESS(rv, rv); + + aTypeOffset = 0; + + if (!JS_DefineElement(aCx, array, index++, val, JSPROP_ENUMERATE)) { + NS_WARNING("Failed to set array element!"); + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + NS_ASSERTION(aPos >= aEnd || (*aPos % eMaxType) == eTerminator, + "Should have found end-of-array marker"); + ++aPos; + + aVal.setObject(*array); + } + else if (*aPos - aTypeOffset == eString) { + nsString key; + DecodeString(aPos, aEnd, key); + if (!xpc::StringToJsval(aCx, key, aVal)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + else if (*aPos - aTypeOffset == eDate) { + double msec = static_cast<double>(DecodeNumber(aPos, aEnd)); + JS::ClippedTime time = JS::TimeClip(msec); + MOZ_ASSERT(msec == time.toDouble(), + "encoding from a Date object not containing an invalid date " + "means we should always have clipped values"); + JSObject* date = JS::NewDateObject(aCx, time); + if (!date) { + IDB_WARNING("Failed to make date!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + aVal.setObject(*date); + } + else if (*aPos - aTypeOffset == eFloat) { + aVal.setDouble(DecodeNumber(aPos, aEnd)); + } + else if (*aPos - aTypeOffset == eBinary) { + JSObject* binary = DecodeBinary(aPos, aEnd, aCx); + if (!binary) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + aVal.setObject(*binary); + } + else { + NS_NOTREACHED("Unknown key type!"); + } + + return NS_OK; +} + +#define ONE_BYTE_LIMIT 0x7E +#define TWO_BYTE_LIMIT (0x3FFF+0x7F) + +#define ONE_BYTE_ADJUST 1 +#define TWO_BYTE_ADJUST (-0x7F) +#define THREE_BYTE_SHIFT 6 + +nsresult +Key::EncodeJSVal(JSContext* aCx, + JS::Handle<JS::Value> aVal, + uint8_t aTypeOffset) +{ + return EncodeJSValInternal(aCx, aVal, aTypeOffset, 0); +} + +void +Key::EncodeString(const nsAString& aString, uint8_t aTypeOffset) +{ + const char16_t* start = aString.BeginReading(); + const char16_t* end = aString.EndReading(); + EncodeString(start, end, aTypeOffset); +} + +template <typename T> +void +Key::EncodeString(const T* aStart, const T* aEnd, uint8_t aTypeOffset) +{ + EncodeAsString(aStart, aEnd, eString + aTypeOffset); +} + +template <typename T> +void +Key::EncodeAsString(const T* aStart, const T* aEnd, uint8_t aType) +{ + // First measure how long the encoded string will be. + + // The +2 is for initial 3 and trailing 0. We'll compensate for multi-byte + // chars below. + uint32_t size = (aEnd - aStart) + 2; + + const T* start = aStart; + const T* end = aEnd; + for (const T* iter = start; iter < end; ++iter) { + if (*iter > ONE_BYTE_LIMIT) { + size += char16_t(*iter) > TWO_BYTE_LIMIT ? 2 : 1; + } + } + + // Allocate memory for the new size + uint32_t oldLen = mBuffer.Length(); + char* buffer; + if (!mBuffer.GetMutableData(&buffer, oldLen + size)) { + return; + } + buffer += oldLen; + + // Write type marker + *(buffer++) = aType; + + // Encode string + for (const T* iter = start; iter < end; ++iter) { + if (*iter <= ONE_BYTE_LIMIT) { + *(buffer++) = *iter + ONE_BYTE_ADJUST; + } + else if (char16_t(*iter) <= TWO_BYTE_LIMIT) { + char16_t c = char16_t(*iter) + TWO_BYTE_ADJUST + 0x8000; + *(buffer++) = (char)(c >> 8); + *(buffer++) = (char)(c & 0xFF); + } + else { + uint32_t c = (uint32_t(*iter) << THREE_BYTE_SHIFT) | 0x00C00000; + *(buffer++) = (char)(c >> 16); + *(buffer++) = (char)(c >> 8); + *(buffer++) = (char)c; + } + } + + // Write end marker + *(buffer++) = eTerminator; + + NS_ASSERTION(buffer == mBuffer.EndReading(), "Wrote wrong number of bytes"); +} + +#ifdef ENABLE_INTL_API +nsresult +Key::EncodeLocaleString(const nsDependentString& aString, uint8_t aTypeOffset, + const nsCString& aLocale) +{ + const int length = aString.Length(); + if (length == 0) { + return NS_OK; + } + const UChar* ustr = reinterpret_cast<const UChar*>(aString.BeginReading()); + + UErrorCode uerror = U_ZERO_ERROR; + UCollator* collator = ucol_open(aLocale.get(), &uerror); + if (NS_WARN_IF(U_FAILURE(uerror))) { + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(collator); + + AutoTArray<uint8_t, 128> keyBuffer; + int32_t sortKeyLength = ucol_getSortKey(collator, ustr, length, + keyBuffer.Elements(), + keyBuffer.Length()); + if (sortKeyLength > (int32_t)keyBuffer.Length()) { + keyBuffer.SetLength(sortKeyLength); + sortKeyLength = ucol_getSortKey(collator, ustr, length, + keyBuffer.Elements(), + sortKeyLength); + } + + ucol_close(collator); + if (NS_WARN_IF(sortKeyLength == 0)) { + return NS_ERROR_FAILURE; + } + + EncodeString(keyBuffer.Elements(), + keyBuffer.Elements()+sortKeyLength, + aTypeOffset); + return NS_OK; +} +#endif + +// static +nsresult +Key::DecodeJSVal(const unsigned char*& aPos, + const unsigned char* aEnd, + JSContext* aCx, + JS::MutableHandle<JS::Value> aVal) +{ + return DecodeJSValInternal(aPos, aEnd, aCx, 0, aVal, 0); +} + +// static +void +Key::DecodeString(const unsigned char*& aPos, const unsigned char* aEnd, + nsString& aString) +{ + NS_ASSERTION(*aPos % eMaxType == eString, "Don't call me!"); + + const unsigned char* buffer = aPos + 1; + + // First measure how big the decoded string will be. + uint32_t size = 0; + const unsigned char* iter; + for (iter = buffer; iter < aEnd && *iter != eTerminator; ++iter) { + if (*iter & 0x80) { + iter += (*iter & 0x40) ? 2 : 1; + } + ++size; + } + + // Set end so that we don't have to check for null termination in the loop + // below + if (iter < aEnd) { + aEnd = iter; + } + + char16_t* out; + if (size && !aString.GetMutableData(&out, size)) { + return; + } + + for (iter = buffer; iter < aEnd;) { + if (!(*iter & 0x80)) { + *out = *(iter++) - ONE_BYTE_ADJUST; + } + else if (!(*iter & 0x40)) { + char16_t c = (char16_t(*(iter++)) << 8); + if (iter < aEnd) { + c |= *(iter++); + } + *out = c - TWO_BYTE_ADJUST - 0x8000; + } + else { + uint32_t c = uint32_t(*(iter++)) << (16 - THREE_BYTE_SHIFT); + if (iter < aEnd) { + c |= uint32_t(*(iter++)) << (8 - THREE_BYTE_SHIFT); + } + if (iter < aEnd) { + c |= *(iter++) >> THREE_BYTE_SHIFT; + } + *out = (char16_t)c; + } + + ++out; + } + + NS_ASSERTION(!size || out == aString.EndReading(), + "Should have written the whole string"); + + aPos = iter + 1; +} + +void +Key::EncodeNumber(double aFloat, uint8_t aType) +{ + // Allocate memory for the new size + uint32_t oldLen = mBuffer.Length(); + char* buffer; + if (!mBuffer.GetMutableData(&buffer, oldLen + 1 + sizeof(double))) { + return; + } + buffer += oldLen; + + *(buffer++) = aType; + + uint64_t bits = BitwiseCast<uint64_t>(aFloat); + // Note: The subtraction from 0 below is necessary to fix + // MSVC build warning C4146 (negating an unsigned value). + const uint64_t signbit = FloatingPoint<double>::kSignBit; + uint64_t number = bits & signbit ? (0 - bits) : (bits | signbit); + + mozilla::BigEndian::writeUint64(buffer, number); +} + +// static +double +Key::DecodeNumber(const unsigned char*& aPos, const unsigned char* aEnd) +{ + NS_ASSERTION(*aPos % eMaxType == eFloat || + *aPos % eMaxType == eDate, "Don't call me!"); + + ++aPos; + + uint64_t number = 0; + memcpy(&number, aPos, std::min<size_t>(sizeof(number), aEnd - aPos)); + number = mozilla::NativeEndian::swapFromBigEndian(number); + + aPos += sizeof(number); + + // Note: The subtraction from 0 below is necessary to fix + // MSVC build warning C4146 (negating an unsigned value). + const uint64_t signbit = FloatingPoint<double>::kSignBit; + uint64_t bits = number & signbit ? (number & ~signbit) : (0 - number); + + return BitwiseCast<double>(bits); +} + +void +Key::EncodeBinary(JSObject* aObject, bool aIsViewObject, uint8_t aTypeOffset) +{ + uint8_t* bufferData; + uint32_t bufferLength; + bool unused; + + if (aIsViewObject) { + js::GetArrayBufferViewLengthAndData(aObject, &bufferLength, &unused, &bufferData); + } else { + js::GetArrayBufferLengthAndData(aObject, &bufferLength, &unused, &bufferData); + } + + EncodeAsString(bufferData, bufferData + bufferLength, eBinary + aTypeOffset); +} + +// static +JSObject* +Key::DecodeBinary(const unsigned char*& aPos, + const unsigned char* aEnd, + JSContext* aCx) +{ + MOZ_ASSERT(*aPos % eMaxType == eBinary, "Don't call me!"); + + const unsigned char* buffer = ++aPos; + + // First measure how big the decoded array buffer will be. + size_t size = 0; + const unsigned char* iter; + for (iter = buffer; iter < aEnd && *iter != eTerminator; ++iter) { + if (*iter & 0x80) { + iter++; + } + ++size; + } + + if (!size) { + return JS_NewArrayBuffer(aCx, 0); + } + + uint8_t* out = static_cast<uint8_t*>(JS_malloc(aCx, size)); + if (NS_WARN_IF(!out)) { + return nullptr; + } + + uint8_t* pos = out; + + // Set end so that we don't have to check for null termination in the loop + // below + if (iter < aEnd) { + aEnd = iter; + } + + for (iter = buffer; iter < aEnd;) { + if (!(*iter & 0x80)) { + *pos = *(iter++) - ONE_BYTE_ADJUST; + } + else { + uint16_t c = (uint16_t(*(iter++)) << 8); + if (iter < aEnd) { + c |= *(iter++); + } + *pos = static_cast<uint8_t>(c - TWO_BYTE_ADJUST - 0x8000); + } + + ++pos; + } + + aPos = iter + 1; + + MOZ_ASSERT(static_cast<size_t>(pos - out) == size, + "Should have written the whole buffer"); + + return JS_NewArrayBufferWithContents(aCx, size, out); +} + +nsresult +Key::BindToStatement(mozIStorageStatement* aStatement, + const nsACString& aParamName) const +{ + nsresult rv; + if (IsUnset()) { + rv = aStatement->BindNullByName(aParamName); + } else { + rv = aStatement->BindBlobByName(aParamName, + reinterpret_cast<const uint8_t*>(mBuffer.get()), mBuffer.Length()); + } + + return NS_SUCCEEDED(rv) ? NS_OK : NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; +} + +nsresult +Key::SetFromStatement(mozIStorageStatement* aStatement, + uint32_t aIndex) +{ + return SetFromSource(aStatement, aIndex); +} + +nsresult +Key::SetFromValueArray(mozIStorageValueArray* aValues, + uint32_t aIndex) +{ + return SetFromSource(aValues, aIndex); +} + +nsresult +Key::SetFromJSVal(JSContext* aCx, + JS::Handle<JS::Value> aVal) +{ + mBuffer.Truncate(); + + if (aVal.isNull() || aVal.isUndefined()) { + Unset(); + return NS_OK; + } + + nsresult rv = EncodeJSVal(aCx, aVal, 0); + if (NS_FAILED(rv)) { + Unset(); + return rv; + } + TrimBuffer(); + + return NS_OK; +} + +nsresult +Key::ToJSVal(JSContext* aCx, + JS::MutableHandle<JS::Value> aVal) const +{ + if (IsUnset()) { + aVal.setUndefined(); + return NS_OK; + } + + const unsigned char* pos = BufferStart(); + nsresult rv = DecodeJSVal(pos, BufferEnd(), aCx, aVal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(pos >= BufferEnd()); + + return NS_OK; +} + +nsresult +Key::ToJSVal(JSContext* aCx, + JS::Heap<JS::Value>& aVal) const +{ + JS::Rooted<JS::Value> value(aCx); + nsresult rv = ToJSVal(aCx, &value); + if (NS_SUCCEEDED(rv)) { + aVal = value; + } + return rv; +} + +nsresult +Key::AppendItem(JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal) +{ + nsresult rv = EncodeJSVal(aCx, aVal, aFirstOfArray ? eMaxType : 0); + if (NS_FAILED(rv)) { + Unset(); + return rv; + } + + return NS_OK; +} + +template <typename T> +nsresult +Key::SetFromSource(T* aSource, uint32_t aIndex) +{ + const uint8_t* data; + uint32_t dataLength = 0; + + nsresult rv = aSource->GetSharedBlob(aIndex, &dataLength, &data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + mBuffer.Assign(reinterpret_cast<const char*>(data), dataLength); + + return NS_OK; +} + +#ifdef DEBUG + +void +Key::Assert(bool aCondition) const +{ + MOZ_ASSERT(aCondition); +} + +#endif // DEBUG + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/Key.h b/dom/indexedDB/Key.h new file mode 100644 index 000000000..856089c97 --- /dev/null +++ b/dom/indexedDB/Key.h @@ -0,0 +1,365 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_key_h__ +#define mozilla_dom_indexeddb_key_h__ + +#include "js/RootingAPI.h" +#include "nsString.h" + +class mozIStorageStatement; +class mozIStorageValueArray; + +namespace IPC { + +template <typename> struct ParamTraits; + +} // namespace IPC + +namespace mozilla { +namespace dom { +namespace indexedDB { + +class Key +{ + friend struct IPC::ParamTraits<Key>; + + nsCString mBuffer; + +public: + enum { + eTerminator = 0, + eFloat = 0x10, + eDate = 0x20, + eString = 0x30, + eBinary = 0x40, + eArray = 0x50, + eMaxType = eArray + }; + + static const uint8_t kMaxArrayCollapse = uint8_t(3); + static const uint8_t kMaxRecursionDepth = uint8_t(64); + + Key() + { + Unset(); + } + + explicit + Key(const nsACString& aBuffer) + : mBuffer(aBuffer) + { } + + Key& + operator=(const nsAString& aString) + { + SetFromString(aString); + return *this; + } + + Key& + operator=(int64_t aInt) + { + SetFromInteger(aInt); + return *this; + } + + bool + operator==(const Key& aOther) const + { + Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid()); + + return mBuffer.Equals(aOther.mBuffer); + } + + bool + operator!=(const Key& aOther) const + { + Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid()); + + return !mBuffer.Equals(aOther.mBuffer); + } + + bool + operator<(const Key& aOther) const + { + Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) < 0; + } + + bool + operator>(const Key& aOther) const + { + Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) > 0; + } + + bool + operator<=(const Key& aOther) const + { + Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) <= 0; + } + + bool + operator>=(const Key& aOther) const + { + Assert(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) >= 0; + } + + void + Unset() + { + mBuffer.SetIsVoid(true); + } + + bool + IsUnset() const + { + return mBuffer.IsVoid(); + } + + bool + IsFloat() const + { + return !IsUnset() && *BufferStart() == eFloat; + } + + bool + IsDate() const + { + return !IsUnset() && *BufferStart() == eDate; + } + + bool + IsString() const + { + return !IsUnset() && *BufferStart() == eString; + } + + bool + IsBinary() const + { + return !IsUnset() && *BufferStart() == eBinary; + } + + bool + IsArray() const + { + return !IsUnset() && *BufferStart() >= eArray; + } + + double + ToFloat() const + { + Assert(IsFloat()); + const unsigned char* pos = BufferStart(); + double res = DecodeNumber(pos, BufferEnd()); + Assert(pos >= BufferEnd()); + return res; + } + + double + ToDateMsec() const + { + Assert(IsDate()); + const unsigned char* pos = BufferStart(); + double res = DecodeNumber(pos, BufferEnd()); + Assert(pos >= BufferEnd()); + return res; + } + + void + ToString(nsString& aString) const + { + Assert(IsString()); + const unsigned char* pos = BufferStart(); + DecodeString(pos, BufferEnd(), aString); + Assert(pos >= BufferEnd()); + } + + void + SetFromString(const nsAString& aString) + { + mBuffer.Truncate(); + EncodeString(aString, 0); + TrimBuffer(); + } + + void + SetFromInteger(int64_t aInt) + { + mBuffer.Truncate(); + EncodeNumber(double(aInt), eFloat); + TrimBuffer(); + } + + nsresult + SetFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal); + + nsresult + ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) const; + + nsresult + ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aVal) const; + + nsresult + AppendItem(JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal); + +#ifdef ENABLE_INTL_API + nsresult + ToLocaleBasedKey(Key& aTarget, const nsCString& aLocale) const; +#endif + + void + FinishArray() + { + TrimBuffer(); + } + + const nsCString& + GetBuffer() const + { + return mBuffer; + } + + nsresult + BindToStatement(mozIStorageStatement* aStatement, + const nsACString& aParamName) const; + + nsresult + SetFromStatement(mozIStorageStatement* aStatement, uint32_t aIndex); + + nsresult + SetFromValueArray(mozIStorageValueArray* aValues, uint32_t aIndex); + + static int16_t + CompareKeys(const Key& aFirst, const Key& aSecond) + { + int32_t result = Compare(aFirst.mBuffer, aSecond.mBuffer); + + if (result < 0) { + return -1; + } + + if (result > 0) { + return 1; + } + + return 0; + } + +private: + const unsigned char* + BufferStart() const + { + return reinterpret_cast<const unsigned char*>(mBuffer.BeginReading()); + } + + const unsigned char* + BufferEnd() const + { + return reinterpret_cast<const unsigned char*>(mBuffer.EndReading()); + } + + // Encoding helper. Trims trailing zeros off of mBuffer as a post-processing + // step. + void + TrimBuffer() + { + const char* end = mBuffer.EndReading() - 1; + while (!*end) { + --end; + } + + mBuffer.Truncate(end + 1 - mBuffer.BeginReading()); + } + + // Encoding functions. These append the encoded value to the end of mBuffer + nsresult + EncodeJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset); + + void + EncodeString(const nsAString& aString, uint8_t aTypeOffset); + + template <typename T> + void + EncodeString(const T* aStart, const T* aEnd, uint8_t aTypeOffset); + + template <typename T> + void + EncodeAsString(const T* aStart, const T* aEnd, uint8_t aType); + +#ifdef ENABLE_INTL_API + nsresult + EncodeLocaleString(const nsDependentString& aString, uint8_t aTypeOffset, + const nsCString& aLocale); +#endif + + void + EncodeNumber(double aFloat, uint8_t aType); + + void + EncodeBinary(JSObject* aObject, bool aIsViewObject, uint8_t aTypeOffset); + + // Decoding functions. aPos points into mBuffer and is adjusted to point + // past the consumed value. + static nsresult + DecodeJSVal(const unsigned char*& aPos, + const unsigned char* aEnd, + JSContext* aCx, + JS::MutableHandle<JS::Value> aVal); + + static void + DecodeString(const unsigned char*& aPos, + const unsigned char* aEnd, + nsString& aString); + + static double + DecodeNumber(const unsigned char*& aPos, const unsigned char* aEnd); + + static JSObject* + DecodeBinary(const unsigned char*& aPos, + const unsigned char* aEnd, + JSContext* aCx); + + nsresult + EncodeJSValInternal(JSContext* aCx, + JS::Handle<JS::Value> aVal, + uint8_t aTypeOffset, + uint16_t aRecursionDepth); + + static nsresult + DecodeJSValInternal(const unsigned char*& aPos, + const unsigned char* aEnd, + JSContext* aCx, + uint8_t aTypeOffset, + JS::MutableHandle<JS::Value> aVal, + uint16_t aRecursionDepth); + + template <typename T> + nsresult + SetFromSource(T* aSource, uint32_t aIndex); + + void + Assert(bool aCondition) const +#ifdef DEBUG + ; +#else + { } +#endif +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_key_h__ diff --git a/dom/indexedDB/KeyPath.cpp b/dom/indexedDB/KeyPath.cpp new file mode 100644 index 000000000..dc8d10668 --- /dev/null +++ b/dom/indexedDB/KeyPath.cpp @@ -0,0 +1,539 @@ +/* -*- 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 "KeyPath.h" +#include "IDBObjectStore.h" +#include "Key.h" +#include "ReportInternalError.h" + +#include "nsCharSeparatedTokenizer.h" +#include "nsJSUtils.h" +#include "xpcpublic.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IDBObjectStoreBinding.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +namespace { + +inline +bool +IgnoreWhitespace(char16_t c) +{ + return false; +} + +typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer; + +bool +IsValidKeyPathString(const nsAString& aKeyPath) +{ + NS_ASSERTION(!aKeyPath.IsVoid(), "What?"); + + KeyPathTokenizer tokenizer(aKeyPath, '.'); + + while (tokenizer.hasMoreTokens()) { + nsString token(tokenizer.nextToken()); + + if (!token.Length()) { + return false; + } + + if (!JS_IsIdentifier(token.get(), token.Length())) { + return false; + } + } + + // If the very last character was a '.', the tokenizer won't give us an empty + // token, but the keyPath is still invalid. + if (!aKeyPath.IsEmpty() && + aKeyPath.CharAt(aKeyPath.Length() - 1) == '.') { + return false; + } + + return true; +} + +enum KeyExtractionOptions { + DoNotCreateProperties, + CreateProperties +}; + +nsresult +GetJSValFromKeyPathString(JSContext* aCx, + const JS::Value& aValue, + const nsAString& aKeyPathString, + JS::Value* aKeyJSVal, + KeyExtractionOptions aOptions, + KeyPath::ExtractOrCreateKeyCallback aCallback, + void* aClosure) +{ + NS_ASSERTION(aCx, "Null pointer!"); + NS_ASSERTION(IsValidKeyPathString(aKeyPathString), + "This will explode!"); + NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties, + "This is not allowed!"); + NS_ASSERTION(aOptions != CreateProperties || aCallback, + "If properties are created, there must be a callback!"); + + nsresult rv = NS_OK; + *aKeyJSVal = aValue; + + KeyPathTokenizer tokenizer(aKeyPathString, '.'); + + nsString targetObjectPropName; + JS::Rooted<JSObject*> targetObject(aCx, nullptr); + JS::Rooted<JS::Value> currentVal(aCx, aValue); + JS::Rooted<JSObject*> obj(aCx); + + while (tokenizer.hasMoreTokens()) { + const nsDependentSubstring& token = tokenizer.nextToken(); + + NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath"); + + const char16_t* keyPathChars = token.BeginReading(); + const size_t keyPathLen = token.Length(); + + bool hasProp; + if (!targetObject) { + // We're still walking the chain of existing objects + // http://w3c.github.io/IndexedDB/#dfn-evaluate-a-key-path-on-a-value + // step 4 substep 1: check for .length on a String value. + if (currentVal.isString() && !tokenizer.hasMoreTokens() && + token.EqualsLiteral("length") && aOptions == DoNotCreateProperties) { + aKeyJSVal->setNumber(double(JS_GetStringLength(currentVal.toString()))); + break; + } + + if (!currentVal.isObject()) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + obj = ¤tVal.toObject(); + + bool ok = JS_HasUCProperty(aCx, obj, keyPathChars, keyPathLen, + &hasProp); + IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + + if (hasProp) { + // Get if the property exists... + JS::Rooted<JS::Value> intermediate(aCx); + bool ok = JS_GetUCProperty(aCx, obj, keyPathChars, keyPathLen, &intermediate); + IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + + // Treat explicitly undefined as an error. + if (intermediate.isUndefined()) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + if (tokenizer.hasMoreTokens()) { + // ...and walk to it if there are more steps... + currentVal = intermediate; + } + else { + // ...otherwise use it as key + *aKeyJSVal = intermediate; + } + } + else { + // If the property doesn't exist, fall into below path of starting + // to define properties, if allowed. + if (aOptions == DoNotCreateProperties) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + targetObject = obj; + targetObjectPropName = token; + } + } + + if (targetObject) { + // We have started inserting new objects or are about to just insert + // the first one. + + aKeyJSVal->setUndefined(); + + if (tokenizer.hasMoreTokens()) { + // If we're not at the end, we need to add a dummy object to the + // chain. + JS::Rooted<JSObject*> dummy(aCx, JS_NewPlainObject(aCx)); + if (!dummy) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + break; + } + + if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), + token.Length(), dummy, JSPROP_ENUMERATE)) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + break; + } + + obj = dummy; + } + else { + JS::Rooted<JSObject*> dummy(aCx, + JS_NewObject(aCx, IDBObjectStore::DummyPropClass())); + if (!dummy) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + break; + } + + if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), + token.Length(), dummy, JSPROP_ENUMERATE)) { + IDB_REPORT_INTERNAL_ERR(); + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + break; + } + + obj = dummy; + } + } + } + + // We guard on rv being a success because we need to run the property + // deletion code below even if we should not be running the callback. + if (NS_SUCCEEDED(rv) && aCallback) { + rv = (*aCallback)(aCx, aClosure); + } + + if (targetObject) { + // If this fails, we lose, and the web page sees a magical property + // appear on the object :-( + JS::ObjectOpResult succeeded; + if (!JS_DeleteUCProperty(aCx, targetObject, + targetObjectPropName.get(), + targetObjectPropName.Length(), + succeeded)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +} // namespace + +// static +nsresult +KeyPath::Parse(const nsAString& aString, KeyPath* aKeyPath) +{ + KeyPath keyPath(0); + keyPath.SetType(STRING); + + if (!keyPath.AppendStringWithValidation(aString)) { + return NS_ERROR_FAILURE; + } + + *aKeyPath = keyPath; + return NS_OK; +} + +//static +nsresult +KeyPath::Parse(const Sequence<nsString>& aStrings, KeyPath* aKeyPath) +{ + KeyPath keyPath(0); + keyPath.SetType(ARRAY); + + for (uint32_t i = 0; i < aStrings.Length(); ++i) { + if (!keyPath.AppendStringWithValidation(aStrings[i])) { + return NS_ERROR_FAILURE; + } + } + + *aKeyPath = keyPath; + return NS_OK; +} + +// static +nsresult +KeyPath::Parse(const Nullable<OwningStringOrStringSequence>& aValue, KeyPath* aKeyPath) +{ + KeyPath keyPath(0); + + aKeyPath->SetType(NONEXISTENT); + + if (aValue.IsNull()) { + *aKeyPath = keyPath; + return NS_OK; + } + + if (aValue.Value().IsString()) { + return Parse(aValue.Value().GetAsString(), aKeyPath); + } + + MOZ_ASSERT(aValue.Value().IsStringSequence()); + + const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence(); + if (seq.Length() == 0) { + return NS_ERROR_FAILURE; + } + return Parse(seq, aKeyPath); +} + +void +KeyPath::SetType(KeyPathType aType) +{ + mType = aType; + mStrings.Clear(); +} + +bool +KeyPath::AppendStringWithValidation(const nsAString& aString) +{ + if (!IsValidKeyPathString(aString)) { + return false; + } + + if (IsString()) { + NS_ASSERTION(mStrings.Length() == 0, "Too many strings!"); + mStrings.AppendElement(aString); + return true; + } + + if (IsArray()) { + mStrings.AppendElement(aString); + return true; + } + + NS_NOTREACHED("What?!"); + return false; +} + +nsresult +KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const +{ + uint32_t len = mStrings.Length(); + JS::Rooted<JS::Value> value(aCx); + + aKey.Unset(); + + for (uint32_t i = 0; i < len; ++i) { + nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i], + value.address(), + DoNotCreateProperties, nullptr, + nullptr); + if (NS_FAILED(rv)) { + return rv; + } + + if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) { + NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset"); + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + } + + aKey.FinishArray(); + + return NS_OK; +} + +nsresult +KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, + JS::Value* aOutVal) const +{ + NS_ASSERTION(IsValid(), "This doesn't make sense!"); + + if (IsString()) { + return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal, + DoNotCreateProperties, nullptr, nullptr); + } + + const uint32_t len = mStrings.Length(); + JS::Rooted<JSObject*> arrayObj(aCx, JS_NewArrayObject(aCx, len)); + if (!arrayObj) { + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::Rooted<JS::Value> value(aCx); + for (uint32_t i = 0; i < len; ++i) { + nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i], + value.address(), + DoNotCreateProperties, nullptr, + nullptr); + if (NS_FAILED(rv)) { + return rv; + } + + if (!JS_DefineElement(aCx, arrayObj, i, value, JSPROP_ENUMERATE)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + aOutVal->setObject(*arrayObj); + return NS_OK; +} + +nsresult +KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue, + Key& aKey, ExtractOrCreateKeyCallback aCallback, + void* aClosure) const +{ + NS_ASSERTION(IsString(), "This doesn't make sense!"); + + JS::Rooted<JS::Value> value(aCx); + + aKey.Unset(); + + nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[0], + value.address(), + CreateProperties, aCallback, + aClosure); + if (NS_FAILED(rv)) { + return rv; + } + + if (NS_FAILED(aKey.AppendItem(aCx, false, value))) { + NS_ASSERTION(aKey.IsUnset(), "Should be unset"); + return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + aKey.FinishArray(); + + return NS_OK; +} + +void +KeyPath::SerializeToString(nsAString& aString) const +{ + NS_ASSERTION(IsValid(), "Check to see if I'm valid first!"); + + if (IsString()) { + aString = mStrings[0]; + return; + } + + if (IsArray()) { + // We use a comma in the beginning to indicate that it's an array of + // key paths. This is to be able to tell a string-keypath from an + // array-keypath which contains only one item. + // It also makes serializing easier :-) + uint32_t len = mStrings.Length(); + for (uint32_t i = 0; i < len; ++i) { + aString.Append(','); + aString.Append(mStrings[i]); + } + + return; + } + + NS_NOTREACHED("What?"); +} + +// static +KeyPath +KeyPath::DeserializeFromString(const nsAString& aString) +{ + KeyPath keyPath(0); + + if (!aString.IsEmpty() && aString.First() == ',') { + keyPath.SetType(ARRAY); + + // We use a comma in the beginning to indicate that it's an array of + // key paths. This is to be able to tell a string-keypath from an + // array-keypath which contains only one item. + nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> tokenizer(aString, ','); + tokenizer.nextToken(); + while (tokenizer.hasMoreTokens()) { + keyPath.mStrings.AppendElement(tokenizer.nextToken()); + } + + return keyPath; + } + + keyPath.SetType(STRING); + keyPath.mStrings.AppendElement(aString); + + return keyPath; +} + +nsresult +KeyPath::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aValue) const +{ + if (IsArray()) { + uint32_t len = mStrings.Length(); + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, len)); + if (!array) { + IDB_WARNING("Failed to make array!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + for (uint32_t i = 0; i < len; ++i) { + JS::Rooted<JS::Value> val(aCx); + nsString tmp(mStrings[i]); + if (!xpc::StringToJsval(aCx, tmp, &val)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (!JS_DefineElement(aCx, array, i, val, JSPROP_ENUMERATE)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + aValue.setObject(*array); + return NS_OK; + } + + if (IsString()) { + nsString tmp(mStrings[0]); + if (!xpc::StringToJsval(aCx, tmp, aValue)) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + return NS_OK; + } + + aValue.setNull(); + return NS_OK; +} + +nsresult +KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const +{ + JS::Rooted<JS::Value> value(aCx); + nsresult rv = ToJSVal(aCx, &value); + if (NS_SUCCEEDED(rv)) { + aValue = value; + } + return rv; +} + +bool +KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const +{ + // Any keypath that passed validation is allowed for non-autoIncrement + // objectStores. + if (!aAutoIncrement) { + return true; + } + + // Array keypaths are not allowed for autoIncrement objectStores. + if (IsArray()) { + return false; + } + + // Neither are empty strings. + if (IsEmpty()) { + return false; + } + + // Everything else is ok. + return true; +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/KeyPath.h b/dom/indexedDB/KeyPath.h new file mode 100644 index 000000000..c133cdc4a --- /dev/null +++ b/dom/indexedDB/KeyPath.h @@ -0,0 +1,127 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_keypath_h__ +#define mozilla_dom_indexeddb_keypath_h__ + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Nullable.h" + +namespace mozilla { +namespace dom { + +class OwningStringOrStringSequence; + +namespace indexedDB { + +class IndexMetadata; +class Key; +class ObjectStoreMetadata; + +class KeyPath +{ + // This private constructor is only to be used by IPDL-generated classes. + friend class IndexMetadata; + friend class ObjectStoreMetadata; + + KeyPath() + : mType(NONEXISTENT) + { + MOZ_COUNT_CTOR(KeyPath); + } + +public: + enum KeyPathType { + NONEXISTENT, + STRING, + ARRAY, + ENDGUARD + }; + + void SetType(KeyPathType aType); + + bool AppendStringWithValidation(const nsAString& aString); + + explicit KeyPath(int aDummy) + : mType(NONEXISTENT) + { + MOZ_COUNT_CTOR(KeyPath); + } + + KeyPath(const KeyPath& aOther) + { + MOZ_COUNT_CTOR(KeyPath); + *this = aOther; + } + + ~KeyPath() + { + MOZ_COUNT_DTOR(KeyPath); + } + + static nsresult + Parse(const nsAString& aString, KeyPath* aKeyPath); + + static nsresult + Parse(const Sequence<nsString>& aStrings, KeyPath* aKeyPath); + + static nsresult + Parse(const Nullable<OwningStringOrStringSequence>& aValue, KeyPath* aKeyPath); + + nsresult + ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const; + + nsresult + ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, + JS::Value* aOutVal) const; + + typedef nsresult + (*ExtractOrCreateKeyCallback)(JSContext* aCx, void* aClosure); + + nsresult + ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue, Key& aKey, + ExtractOrCreateKeyCallback aCallback, + void* aClosure) const; + + inline bool IsValid() const { + return mType != NONEXISTENT; + } + + inline bool IsArray() const { + return mType == ARRAY; + } + + inline bool IsString() const { + return mType == STRING; + } + + inline bool IsEmpty() const { + return mType == STRING && mStrings[0].IsEmpty(); + } + + bool operator==(const KeyPath& aOther) const + { + return mType == aOther.mType && mStrings == aOther.mStrings; + } + + void SerializeToString(nsAString& aString) const; + static KeyPath DeserializeFromString(const nsAString& aString); + + nsresult ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aValue) const; + nsresult ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const; + + bool IsAllowedForObjectStore(bool aAutoIncrement) const; + + KeyPathType mType; + + nsTArray<nsString> mStrings; +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_keypath_h__ diff --git a/dom/indexedDB/PBackgroundIDBCursor.ipdl b/dom/indexedDB/PBackgroundIDBCursor.ipdl new file mode 100644 index 000000000..62db23837 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBCursor.ipdl @@ -0,0 +1,100 @@ +/* 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 protocol PBackgroundIDBTransaction; +include protocol PBackgroundIDBVersionChangeTransaction; +include protocol PBackgroundMutableFile; +include protocol PBlob; + +include PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; + +using struct mozilla::void_t + from "ipc/IPCMessageUtils.h"; + +using class mozilla::dom::indexedDB::Key + from "mozilla/dom/indexedDB/Key.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct ContinueParams +{ + Key key; +}; + +struct ContinuePrimaryKeyParams +{ + Key key; + Key primaryKey; +}; + +struct AdvanceParams +{ + uint32_t count; +}; + +union CursorRequestParams +{ + ContinueParams; + ContinuePrimaryKeyParams; + AdvanceParams; +}; + +struct ObjectStoreCursorResponse +{ + Key key; + SerializedStructuredCloneReadInfo cloneInfo; +}; + +struct ObjectStoreKeyCursorResponse +{ + Key key; +}; + +struct IndexCursorResponse +{ + Key key; + Key sortKey; + Key objectKey; + SerializedStructuredCloneReadInfo cloneInfo; +}; + +struct IndexKeyCursorResponse +{ + Key key; + Key sortKey; + Key objectKey; +}; + +union CursorResponse +{ + void_t; + nsresult; + ObjectStoreCursorResponse[]; + ObjectStoreKeyCursorResponse; + IndexCursorResponse; + IndexKeyCursorResponse; +}; + +protocol PBackgroundIDBCursor +{ + manager PBackgroundIDBTransaction or PBackgroundIDBVersionChangeTransaction; + +parent: + async DeleteMe(); + + async Continue(CursorRequestParams params); + +child: + async __delete__(); + + async Response(CursorResponse response); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBDatabase.ipdl b/dom/indexedDB/PBackgroundIDBDatabase.ipdl new file mode 100644 index 000000000..967d837ba --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBDatabase.ipdl @@ -0,0 +1,87 @@ +/* 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 protocol PBackgroundIDBDatabaseFile; +include protocol PBackgroundIDBDatabaseRequest; +include protocol PBackgroundIDBFactory; +include protocol PBackgroundIDBTransaction; +include protocol PBackgroundIDBVersionChangeTransaction; +include protocol PBackgroundMutableFile; +include protocol PBlob; + +include InputStreamParams; +include PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; + +using struct mozilla::null_t + from "ipc/IPCMessageUtils.h"; + +using mozilla::dom::IDBTransaction::Mode + from "mozilla/dom/IDBTransaction.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct CreateFileParams +{ + nsString name; + nsString type; +}; + +union DatabaseRequestParams +{ + CreateFileParams; +}; + +union NullableVersion +{ + null_t; + uint64_t; +}; + +sync protocol PBackgroundIDBDatabase +{ + manager PBackgroundIDBFactory; + + manages PBackgroundIDBDatabaseFile; + manages PBackgroundIDBDatabaseRequest; + manages PBackgroundIDBTransaction; + manages PBackgroundIDBVersionChangeTransaction; + manages PBackgroundMutableFile; + +parent: + async DeleteMe(); + + async Blocked(); + + async Close(); + + async PBackgroundIDBDatabaseFile(PBlob blob); + + async PBackgroundIDBDatabaseRequest(DatabaseRequestParams params); + + async PBackgroundIDBTransaction(nsString[] objectStoreNames, Mode mode); + +child: + async __delete__(); + + async VersionChange(uint64_t oldVersion, NullableVersion newVersion); + + async Invalidate(); + + async CloseAfterInvalidationComplete(); + + async PBackgroundIDBVersionChangeTransaction(uint64_t currentVersion, + uint64_t requestedVersion, + int64_t nextObjectStoreId, + int64_t nextIndexId); + + async PBackgroundMutableFile(nsString name, nsString type); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBDatabaseFile.ipdl b/dom/indexedDB/PBackgroundIDBDatabaseFile.ipdl new file mode 100644 index 000000000..e2f7c9f85 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBDatabaseFile.ipdl @@ -0,0 +1,21 @@ +/* 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 protocol PBackgroundIDBDatabase; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +protocol PBackgroundIDBDatabaseFile +{ + manager PBackgroundIDBDatabase; + +parent: + async __delete__(); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBDatabaseRequest.ipdl b/dom/indexedDB/PBackgroundIDBDatabaseRequest.ipdl new file mode 100644 index 000000000..c3b342048 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBDatabaseRequest.ipdl @@ -0,0 +1,33 @@ +/* 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 protocol PBackgroundIDBDatabase; +include protocol PBackgroundMutableFile; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct CreateFileRequestResponse +{ + PBackgroundMutableFile mutableFile; +}; + +union DatabaseRequestResponse +{ + nsresult; + CreateFileRequestResponse; +}; + +protocol PBackgroundIDBDatabaseRequest +{ + manager PBackgroundIDBDatabase; + +child: + async __delete__(DatabaseRequestResponse response); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBFactory.ipdl b/dom/indexedDB/PBackgroundIDBFactory.ipdl new file mode 100644 index 000000000..1e81e324d --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBFactory.ipdl @@ -0,0 +1,66 @@ +/* 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 protocol PBackground; +include protocol PBackgroundIDBDatabase; +include protocol PBackgroundIDBFactoryRequest; + +include PBackgroundIDBSharedTypes; +include PBackgroundSharedTypes; + +include "mozilla/dom/quota/SerializationHelpers.h"; + +using struct mozilla::void_t + from "ipc/IPCMessageUtils.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct CommonFactoryRequestParams +{ + DatabaseMetadata metadata; + PrincipalInfo principalInfo; +}; + +struct OpenDatabaseRequestParams +{ + CommonFactoryRequestParams commonParams; +}; + +struct DeleteDatabaseRequestParams +{ + CommonFactoryRequestParams commonParams; +}; + +union FactoryRequestParams +{ + OpenDatabaseRequestParams; + DeleteDatabaseRequestParams; +}; + +sync protocol PBackgroundIDBFactory +{ + manager PBackground; + + manages PBackgroundIDBDatabase; + manages PBackgroundIDBFactoryRequest; + +parent: + async DeleteMe(); + + async PBackgroundIDBFactoryRequest(FactoryRequestParams params); + + async IncrementLoggingRequestSerialNumber(); + +child: + async __delete__(); + + async PBackgroundIDBDatabase(DatabaseSpec spec, + PBackgroundIDBFactoryRequest request); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBFactoryRequest.ipdl b/dom/indexedDB/PBackgroundIDBFactoryRequest.ipdl new file mode 100644 index 000000000..fc74807fe --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBFactoryRequest.ipdl @@ -0,0 +1,48 @@ +/* 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 protocol PBackgroundIDBFactory; +include protocol PBackgroundIDBDatabase; + +include PBackgroundSharedTypes; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct OpenDatabaseRequestResponse +{ + PBackgroundIDBDatabase database; +}; + +struct DeleteDatabaseRequestResponse +{ + uint64_t previousVersion; +}; + +union FactoryRequestResponse +{ + nsresult; + OpenDatabaseRequestResponse; + DeleteDatabaseRequestResponse; +}; + +protocol PBackgroundIDBFactoryRequest +{ + manager PBackgroundIDBFactory; + +child: + async __delete__(FactoryRequestResponse response); + + async PermissionChallenge(PrincipalInfo principalInfo); + + async Blocked(uint64_t currentVersion); + +parent: + async PermissionRetry(); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBRequest.ipdl b/dom/indexedDB/PBackgroundIDBRequest.ipdl new file mode 100644 index 000000000..f5831a572 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBRequest.ipdl @@ -0,0 +1,169 @@ +/* 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 protocol PBackgroundIDBTransaction; +include protocol PBackgroundIDBVersionChangeTransaction; +include protocol PBackgroundMutableFile; +include protocol PBlob; + +include PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; + +using struct mozilla::void_t + from "ipc/IPCMessageUtils.h"; + +using class mozilla::dom::indexedDB::Key + from "mozilla/dom/indexedDB/Key.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct ObjectStoreAddResponse +{ + Key key; +}; + +struct ObjectStorePutResponse +{ + Key key; +}; + +struct ObjectStoreGetResponse +{ + SerializedStructuredCloneReadInfo cloneInfo; +}; + +struct ObjectStoreGetKeyResponse +{ + Key key; +}; + +struct ObjectStoreGetAllResponse +{ + SerializedStructuredCloneReadInfo[] cloneInfos; +}; + +struct ObjectStoreGetAllKeysResponse +{ + Key[] keys; +}; + +struct ObjectStoreDeleteResponse +{ }; + +struct ObjectStoreClearResponse +{ }; + +struct ObjectStoreCountResponse +{ + uint64_t count; +}; + +struct IndexGetResponse +{ + SerializedStructuredCloneReadInfo cloneInfo; +}; + +struct IndexGetKeyResponse +{ + Key key; +}; + +struct IndexGetAllResponse +{ + SerializedStructuredCloneReadInfo[] cloneInfos; +}; + +struct IndexGetAllKeysResponse +{ + Key[] keys; +}; + +struct IndexCountResponse +{ + uint64_t count; +}; + +union RequestResponse +{ + nsresult; + ObjectStoreGetResponse; + ObjectStoreGetKeyResponse; + ObjectStoreAddResponse; + ObjectStorePutResponse; + ObjectStoreDeleteResponse; + ObjectStoreClearResponse; + ObjectStoreCountResponse; + ObjectStoreGetAllResponse; + ObjectStoreGetAllKeysResponse; + IndexGetResponse; + IndexGetKeyResponse; + IndexGetAllResponse; + IndexGetAllKeysResponse; + IndexCountResponse; +}; + +struct WasmModulePreprocessInfo +{ + SerializedStructuredCloneFile[] files; +}; + +struct ObjectStoreGetPreprocessParams +{ + WasmModulePreprocessInfo preprocessInfo; +}; + +struct ObjectStoreGetAllPreprocessParams +{ + WasmModulePreprocessInfo[] preprocessInfos; +}; + +union PreprocessParams +{ + ObjectStoreGetPreprocessParams; + ObjectStoreGetAllPreprocessParams; +}; + +struct ObjectStoreGetPreprocessResponse +{ +}; + +struct ObjectStoreGetAllPreprocessResponse +{ +}; + +// The nsresult is used if an error occurs for any preprocess request type. +// The specific response types are sent on success. +union PreprocessResponse +{ + nsresult; + ObjectStoreGetPreprocessResponse; + ObjectStoreGetAllPreprocessResponse; +}; + +protocol PBackgroundIDBRequest +{ + manager PBackgroundIDBTransaction or PBackgroundIDBVersionChangeTransaction; + +parent: + async Continue(PreprocessResponse response); + +child: + async __delete__(RequestResponse response); + + // Preprocess is used in cases where response processing needs to do something + // asynchronous off of the child actor's thread before returning the actual + // result to user code. This is necessary because RequestResponse processing + // occurs in __delete__ and the PBackgroundIDBRequest implementations' + // life-cycles are controlled by IPC and are not otherwise reference counted. + // By introducing the (optional) Preprocess/Continue steps reference counting + // or the introduction of additional runnables are avoided. + async Preprocess(PreprocessParams params); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBSharedTypes.ipdlh b/dom/indexedDB/PBackgroundIDBSharedTypes.ipdlh new file mode 100644 index 000000000..49ac75428 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBSharedTypes.ipdlh @@ -0,0 +1,306 @@ +/* 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 protocol PBackgroundIDBDatabaseFile; +include protocol PBackgroundMutableFile; +include protocol PBlob; + +include DOMTypes; +include ProtocolTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; +include "mozilla/dom/quota/SerializationHelpers.h"; + +using struct mozilla::null_t + from "ipc/IPCMessageUtils.h"; + +using struct mozilla::void_t + from "ipc/IPCMessageUtils.h"; + +using mozilla::dom::IDBCursor::Direction + from "mozilla/dom/IDBCursor.h"; + +using mozilla::dom::indexedDB::StructuredCloneFile::FileType + from "mozilla/dom/IndexedDatabase.h"; + +using class mozilla::dom::indexedDB::Key + from "mozilla/dom/indexedDB/Key.h"; + +using class mozilla::dom::indexedDB::KeyPath + from "mozilla/dom/indexedDB/KeyPath.h"; + +using mozilla::dom::quota::PersistenceType + from "mozilla/dom/quota/PersistenceType.h"; + +using mozilla::SerializedStructuredCloneBuffer + from "ipc/IPCMessageUtils.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct SerializedKeyRange +{ + Key lower; + Key upper; + bool lowerOpen; + bool upperOpen; + bool isOnly; +}; + +union BlobOrMutableFile +{ + null_t; + PBlob; + PBackgroundMutableFile; +}; + +struct SerializedStructuredCloneFile +{ + BlobOrMutableFile file; + FileType type; +}; + +struct SerializedStructuredCloneReadInfo +{ + SerializedStructuredCloneBuffer data; + SerializedStructuredCloneFile[] files; + bool hasPreprocessInfo; +}; + +struct SerializedStructuredCloneWriteInfo +{ + SerializedStructuredCloneBuffer data; + uint64_t offsetToKeyProp; +}; + +struct IndexUpdateInfo +{ + int64_t indexId; + Key value; + Key localizedValue; +}; + +union OptionalKeyRange +{ + SerializedKeyRange; + void_t; +}; + +struct DatabaseMetadata +{ + nsString name; + uint64_t version; + PersistenceType persistenceType; +}; + +struct ObjectStoreMetadata +{ + int64_t id; + nsString name; + KeyPath keyPath; + bool autoIncrement; +}; + +struct IndexMetadata +{ + int64_t id; + nsString name; + KeyPath keyPath; + nsCString locale; + bool unique; + bool multiEntry; + bool autoLocale; +}; + +struct DatabaseSpec +{ + DatabaseMetadata metadata; + ObjectStoreSpec[] objectStores; +}; + +struct ObjectStoreSpec +{ + ObjectStoreMetadata metadata; + IndexMetadata[] indexes; +}; + +struct ObjectStoreOpenCursorParams +{ + int64_t objectStoreId; + OptionalKeyRange optionalKeyRange; + Direction direction; +}; + +struct ObjectStoreOpenKeyCursorParams +{ + int64_t objectStoreId; + OptionalKeyRange optionalKeyRange; + Direction direction; +}; + +struct IndexOpenCursorParams +{ + int64_t objectStoreId; + int64_t indexId; + OptionalKeyRange optionalKeyRange; + Direction direction; +}; + +struct IndexOpenKeyCursorParams +{ + int64_t objectStoreId; + int64_t indexId; + OptionalKeyRange optionalKeyRange; + Direction direction; +}; + +union OpenCursorParams +{ + ObjectStoreOpenCursorParams; + ObjectStoreOpenKeyCursorParams; + IndexOpenCursorParams; + IndexOpenKeyCursorParams; +}; + +union DatabaseOrMutableFile +{ + PBackgroundIDBDatabaseFile; + PBackgroundMutableFile; +}; + +struct FileAddInfo +{ + DatabaseOrMutableFile file; + FileType type; +}; + +struct ObjectStoreAddPutParams +{ + int64_t objectStoreId; + SerializedStructuredCloneWriteInfo cloneInfo; + Key key; + IndexUpdateInfo[] indexUpdateInfos; + FileAddInfo[] fileAddInfos; +}; + +struct ObjectStoreAddParams +{ + ObjectStoreAddPutParams commonParams; +}; + +struct ObjectStorePutParams +{ + ObjectStoreAddPutParams commonParams; +}; + +struct ObjectStoreGetParams +{ + int64_t objectStoreId; + SerializedKeyRange keyRange; +}; + +struct ObjectStoreGetKeyParams +{ + int64_t objectStoreId; + SerializedKeyRange keyRange; +}; + +struct ObjectStoreGetAllParams +{ + int64_t objectStoreId; + OptionalKeyRange optionalKeyRange; + uint32_t limit; +}; + +struct ObjectStoreGetAllKeysParams +{ + int64_t objectStoreId; + OptionalKeyRange optionalKeyRange; + uint32_t limit; +}; + +struct ObjectStoreDeleteParams +{ + int64_t objectStoreId; + SerializedKeyRange keyRange; +}; + +struct ObjectStoreClearParams +{ + int64_t objectStoreId; +}; + +struct ObjectStoreCountParams +{ + int64_t objectStoreId; + OptionalKeyRange optionalKeyRange; +}; + +struct IndexGetParams +{ + int64_t objectStoreId; + int64_t indexId; + SerializedKeyRange keyRange; +}; + +struct IndexGetKeyParams +{ + int64_t objectStoreId; + int64_t indexId; + SerializedKeyRange keyRange; +}; + +struct IndexGetAllParams +{ + int64_t objectStoreId; + int64_t indexId; + OptionalKeyRange optionalKeyRange; + uint32_t limit; +}; + +struct IndexGetAllKeysParams +{ + int64_t objectStoreId; + int64_t indexId; + OptionalKeyRange optionalKeyRange; + uint32_t limit; +}; + +struct IndexCountParams +{ + int64_t objectStoreId; + int64_t indexId; + OptionalKeyRange optionalKeyRange; +}; + +union RequestParams +{ + ObjectStoreAddParams; + ObjectStorePutParams; + ObjectStoreGetParams; + ObjectStoreGetKeyParams; + ObjectStoreGetAllParams; + ObjectStoreGetAllKeysParams; + ObjectStoreDeleteParams; + ObjectStoreClearParams; + ObjectStoreCountParams; + IndexGetParams; + IndexGetKeyParams; + IndexGetAllParams; + IndexGetAllKeysParams; + IndexCountParams; +}; + +struct LoggingInfo +{ + nsID backgroundChildLoggingId; + int64_t nextTransactionSerialNumber; + int64_t nextVersionChangeTransactionSerialNumber; + uint64_t nextRequestSerialNumber; +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBTransaction.ipdl b/dom/indexedDB/PBackgroundIDBTransaction.ipdl new file mode 100644 index 000000000..355f07761 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBTransaction.ipdl @@ -0,0 +1,42 @@ +/* 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 protocol PBackgroundIDBCursor; +include protocol PBackgroundIDBDatabase; +include protocol PBackgroundIDBDatabaseFile; +include protocol PBackgroundIDBRequest; +include protocol PBackgroundMutableFile; + +include PBackgroundIDBSharedTypes; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +protocol PBackgroundIDBTransaction +{ + manager PBackgroundIDBDatabase; + + manages PBackgroundIDBCursor; + manages PBackgroundIDBRequest; + +parent: + async DeleteMe(); + + async Commit(); + async Abort(nsresult resultCode); + + async PBackgroundIDBCursor(OpenCursorParams params); + + async PBackgroundIDBRequest(RequestParams params); + +child: + async __delete__(); + + async Complete(nsresult result); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl b/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl new file mode 100644 index 000000000..e3cb7cfb6 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl @@ -0,0 +1,56 @@ +/* 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 protocol PBackgroundIDBCursor; +include protocol PBackgroundIDBDatabase; +include protocol PBackgroundIDBDatabaseFile; +include protocol PBackgroundIDBRequest; +include protocol PBackgroundMutableFile; +include protocol PBlob; + +include PBackgroundIDBSharedTypes; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +protocol PBackgroundIDBVersionChangeTransaction +{ + manager PBackgroundIDBDatabase; + + manages PBackgroundIDBCursor; + manages PBackgroundIDBRequest; + +parent: + async DeleteMe(); + + async Commit(); + async Abort(nsresult resultCode); + + async CreateObjectStore(ObjectStoreMetadata metadata); + async DeleteObjectStore(int64_t objectStoreId); + async RenameObjectStore(int64_t objectStoreId, + nsString name); + + async CreateIndex(int64_t objectStoreId, + IndexMetadata metadata); + async DeleteIndex(int64_t objectStoreId, + int64_t indexId); + async RenameIndex(int64_t objectStoreId, + int64_t indexId, + nsString name); + + async PBackgroundIDBCursor(OpenCursorParams params); + + async PBackgroundIDBRequest(RequestParams params); + +child: + async __delete__(); + + async Complete(nsresult result); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIndexedDBUtils.ipdl b/dom/indexedDB/PBackgroundIndexedDBUtils.ipdl new file mode 100644 index 000000000..aac20e1a4 --- /dev/null +++ b/dom/indexedDB/PBackgroundIndexedDBUtils.ipdl @@ -0,0 +1,37 @@ +/* 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 protocol PBackground; + +include "mozilla/dom/quota/SerializationHelpers.h"; + +using mozilla::dom::quota::PersistenceType + from "mozilla/dom/quota/PersistenceType.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +sync protocol PBackgroundIndexedDBUtils +{ + manager PBackground; + +parent: + async DeleteMe(); + + // Use only for testing! + sync GetFileReferences(PersistenceType persistenceType, + nsCString origin, + nsString databaseName, + int64_t fileId) + returns (int32_t refCnt, int32_t dBRefCnt, int32_t sliceRefCnt, + bool result); + +child: + async __delete__(); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PIndexedDBPermissionRequest.ipdl b/dom/indexedDB/PIndexedDBPermissionRequest.ipdl new file mode 100644 index 000000000..779793731 --- /dev/null +++ b/dom/indexedDB/PIndexedDBPermissionRequest.ipdl @@ -0,0 +1,27 @@ +/* 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 protocol PBrowser; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +protocol PIndexedDBPermissionRequest +{ + manager PBrowser; + +child: + /** + * Called when the user makes a choice or the permission request times out. + * + * @param permission + * The permission result (see nsIPermissionManager.idl for valid values). + */ + async __delete__(uint32_t permission); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PermissionRequestBase.cpp b/dom/indexedDB/PermissionRequestBase.cpp new file mode 100644 index 000000000..0bd13dec3 --- /dev/null +++ b/dom/indexedDB/PermissionRequestBase.cpp @@ -0,0 +1,269 @@ +/* -*- 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 "PermissionRequestBase.h" + +#include "MainThreadUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Services.h" +#include "mozilla/dom/Element.h" +#include "nsIDOMWindow.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsPIDOMWindow.h" +#include "nsXULAppAPI.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +using namespace mozilla::services; + +namespace { + +#define IDB_PREFIX "indexedDB" +#define TOPIC_PREFIX IDB_PREFIX "-permissions-" + +const char kPermissionString[] = IDB_PREFIX; + +const char kPermissionPromptTopic[] = TOPIC_PREFIX "prompt"; + +#ifdef DEBUG +const char kPermissionResponseTopic[] = TOPIC_PREFIX "response"; +#endif + +#undef TOPIC_PREFIX +#undef IDB_PREFIX + +const uint32_t kPermissionDefault = nsIPermissionManager::UNKNOWN_ACTION; + +void +AssertSanity() +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); +} + +} // namespace + +PermissionRequestBase::PermissionRequestBase(Element* aOwnerElement, + nsIPrincipal* aPrincipal) + : mOwnerElement(aOwnerElement) + , mPrincipal(aPrincipal) +{ + AssertSanity(); + MOZ_ASSERT(aOwnerElement); + MOZ_ASSERT(aPrincipal); +} + +PermissionRequestBase::~PermissionRequestBase() +{ + AssertSanity(); +} + +// static +nsresult +PermissionRequestBase::GetCurrentPermission(nsIPrincipal* aPrincipal, + PermissionValue* aCurrentValue) +{ + AssertSanity(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCurrentValue); + + nsCOMPtr<nsIPermissionManager> permMan = GetPermissionManager(); + if (NS_WARN_IF(!permMan)) { + return NS_ERROR_FAILURE; + } + + uint32_t intPermission; + nsresult rv = permMan->TestExactPermissionFromPrincipal( + aPrincipal, + kPermissionString, + &intPermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + PermissionValue permission = + PermissionValueForIntPermission(intPermission); + + MOZ_ASSERT(permission == kPermissionAllowed || + permission == kPermissionDenied || + permission == kPermissionPrompt); + + *aCurrentValue = permission; + return NS_OK; +} + +// static +auto +PermissionRequestBase::PermissionValueForIntPermission(uint32_t aIntPermission) + -> PermissionValue +{ + AssertSanity(); + + switch (aIntPermission) { + case kPermissionDefault: + return kPermissionPrompt; + case kPermissionAllowed: + return kPermissionAllowed; + case kPermissionDenied: + return kPermissionDenied; + default: + MOZ_CRASH("Bad permission!"); + } + + MOZ_CRASH("Should never get here!"); +} + +nsresult +PermissionRequestBase::PromptIfNeeded(PermissionValue* aCurrentValue) +{ + AssertSanity(); + MOZ_ASSERT(aCurrentValue); + MOZ_ASSERT(mPrincipal); + + // Tricky, we want to release the window and principal in all cases except + // when we successfully prompt. + nsCOMPtr<Element> element; + mOwnerElement.swap(element); + + nsCOMPtr<nsIPrincipal> principal; + mPrincipal.swap(principal); + + PermissionValue currentValue; + nsresult rv = GetCurrentPermission(principal, ¤tValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(currentValue != kPermissionDefault); + + if (currentValue == kPermissionPrompt) { + nsCOMPtr<nsIObserverService> obsSvc = GetObserverService(); + if (NS_WARN_IF(!obsSvc)) { + return NS_ERROR_FAILURE; + } + + // We're about to prompt so swap the members back. + element.swap(mOwnerElement); + principal.swap(mPrincipal); + + rv = obsSvc->NotifyObservers(static_cast<nsIObserver*>(this), + kPermissionPromptTopic, + nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Finally release if we failed the prompt. + mOwnerElement = nullptr; + mPrincipal = nullptr; + return rv; + } + } + + *aCurrentValue = currentValue; + return NS_OK; +} + +void +PermissionRequestBase::SetExplicitPermission(nsIPrincipal* aPrincipal, + uint32_t aIntPermission) +{ + AssertSanity(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aIntPermission == kPermissionAllowed || + aIntPermission == kPermissionDenied); + + nsCOMPtr<nsIPermissionManager> permMan = GetPermissionManager(); + if (NS_WARN_IF(!permMan)) { + return; + } + + nsresult rv = permMan->AddFromPrincipal(aPrincipal, + kPermissionString, + aIntPermission, + nsIPermissionManager::EXPIRE_NEVER, + /* aExpireTime */ 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +NS_IMPL_ISUPPORTS(PermissionRequestBase, nsIObserver, nsIInterfaceRequestor) + +NS_IMETHODIMP +PermissionRequestBase::GetInterface(const nsIID& aIID, + void** aResult) +{ + AssertSanity(); + + if (aIID.Equals(NS_GET_IID(nsIObserver))) { + return QueryInterface(aIID, aResult); + } + + if (aIID.Equals(NS_GET_IID(nsIDOMNode)) && mOwnerElement) { + return mOwnerElement->QueryInterface(aIID, aResult); + } + + *aResult = nullptr; + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +PermissionRequestBase::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + AssertSanity(); + MOZ_ASSERT(!strcmp(aTopic, kPermissionResponseTopic)); + MOZ_ASSERT(mOwnerElement); + MOZ_ASSERT(mPrincipal); + + nsCOMPtr<Element> element; + element.swap(mOwnerElement); + + nsCOMPtr<nsIPrincipal> principal; + mPrincipal.swap(principal); + + nsresult rv; + uint32_t promptResult = nsDependentString(aData).ToInteger(&rv); + MOZ_ALWAYS_SUCCEEDS(rv); + + // The UI prompt code will only return one of these three values. We have to + // transform it to our values. + MOZ_ASSERT(promptResult == kPermissionDefault || + promptResult == kPermissionAllowed || + promptResult == kPermissionDenied); + + if (promptResult != kPermissionDefault) { + // Save explicitly allowed or denied permissions now. + SetExplicitPermission(principal, promptResult); + } + + PermissionValue permission; + switch (promptResult) { + case kPermissionDefault: + permission = kPermissionPrompt; + break; + + case kPermissionAllowed: + permission = kPermissionAllowed; + break; + + case kPermissionDenied: + permission = kPermissionDenied; + break; + + default: + MOZ_CRASH("Bad prompt result!"); + } + + OnPromptComplete(permission); + return NS_OK; +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PermissionRequestBase.h b/dom/indexedDB/PermissionRequestBase.h new file mode 100644 index 000000000..47ed6c9ca --- /dev/null +++ b/dom/indexedDB/PermissionRequestBase.h @@ -0,0 +1,81 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_permissionrequestbase_h__ +#define mozilla_dom_indexeddb_permissionrequestbase_h__ + +#include "mozilla/Attributes.h" +#include "nsCOMPtr.h" +#include "nsIInterfaceRequestor.h" +#include "nsIObserver.h" +#include "nsIPermissionManager.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +class nsIPrincipal; + +namespace mozilla { +namespace dom { + +class Element; + +namespace indexedDB { + +class PermissionRequestBase + : public nsIObserver + , public nsIInterfaceRequestor +{ + nsCOMPtr<Element> mOwnerElement; + nsCOMPtr<nsIPrincipal> mPrincipal; + +public: + enum PermissionValue { + kPermissionAllowed = nsIPermissionManager::ALLOW_ACTION, + kPermissionDenied = nsIPermissionManager::DENY_ACTION, + kPermissionPrompt = nsIPermissionManager::PROMPT_ACTION + }; + + NS_DECL_ISUPPORTS + + // This function will not actually prompt. It will never return + // kPermissionDefault but will instead translate the permission manager value + // into the correct value for the given type. + static nsresult + GetCurrentPermission(nsIPrincipal* aPrincipal, + PermissionValue* aCurrentValue); + + static PermissionValue + PermissionValueForIntPermission(uint32_t aIntPermission); + + // This function will prompt if needed. It may only be called once. + nsresult + PromptIfNeeded(PermissionValue* aCurrentValue); + +protected: + PermissionRequestBase(Element* aOwnerElement, + nsIPrincipal* aPrincipal); + + // Reference counted. + virtual + ~PermissionRequestBase(); + + virtual void + OnPromptComplete(PermissionValue aPermissionValue) = 0; + +private: + void + SetExplicitPermission(nsIPrincipal* aPrincipal, + uint32_t aIntPermission); + + NS_DECL_NSIOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_permissionrequestbase_h__ diff --git a/dom/indexedDB/ProfilerHelpers.h b/dom/indexedDB/ProfilerHelpers.h new file mode 100644 index 000000000..63fdafcce --- /dev/null +++ b/dom/indexedDB/ProfilerHelpers.h @@ -0,0 +1,350 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_profilerhelpers_h__ +#define mozilla_dom_indexeddb_profilerhelpers_h__ + +// This file is not exported and is only meant to be included in IndexedDB +// source files. + +#include "BackgroundChildImpl.h" +#include "GeckoProfiler.h" +#include "IDBCursor.h" +#include "IDBDatabase.h" +#include "IDBIndex.h" +#include "IDBKeyRange.h" +#include "IDBObjectStore.h" +#include "IDBTransaction.h" +#include "IndexedDatabaseManager.h" +#include "Key.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsDebug.h" +#include "nsID.h" +#include "nsIDOMEvent.h" +#include "nsString.h" +#include "mozilla/Logging.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +class MOZ_STACK_CLASS LoggingIdString final + : public nsAutoCString +{ +public: + LoggingIdString() + { + using mozilla::ipc::BackgroundChildImpl; + + if (IndexedDatabaseManager::GetLoggingMode() != + IndexedDatabaseManager::Logging_Disabled) { + const BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + if (threadLocal) { + const ThreadLocal* idbThreadLocal = threadLocal->mIndexedDBThreadLocal; + if (idbThreadLocal) { + Assign(idbThreadLocal->IdString()); + } + } + } + } + + explicit + LoggingIdString(const nsID& aID) + { + static_assert(NSID_LENGTH > 1, "NSID_LENGTH is set incorrectly!"); + static_assert(NSID_LENGTH <= kDefaultStorageSize, + "nID string won't fit in our storage!"); + MOZ_ASSERT(Capacity() > NSID_LENGTH); + + if (IndexedDatabaseManager::GetLoggingMode() != + IndexedDatabaseManager::Logging_Disabled) { + // NSID_LENGTH counts the null terminator, SetLength() does not. + SetLength(NSID_LENGTH - 1); + + aID.ToProvidedString( + *reinterpret_cast<char(*)[NSID_LENGTH]>(BeginWriting())); + } + } +}; + +class MOZ_STACK_CLASS LoggingString final + : public nsAutoCString +{ + static const char kQuote = '\"'; + static const char kOpenBracket = '['; + static const char kCloseBracket = ']'; + static const char kOpenParen = '('; + static const char kCloseParen = ')'; + +public: + explicit + LoggingString(IDBDatabase* aDatabase) + : nsAutoCString(kQuote) + { + MOZ_ASSERT(aDatabase); + + AppendUTF16toUTF8(aDatabase->Name(), *this); + Append(kQuote); + } + + explicit + LoggingString(IDBTransaction* aTransaction) + : nsAutoCString(kOpenBracket) + { + MOZ_ASSERT(aTransaction); + + NS_NAMED_LITERAL_CSTRING(kCommaSpace, ", "); + + const nsTArray<nsString>& stores = aTransaction->ObjectStoreNamesInternal(); + + for (uint32_t count = stores.Length(), index = 0; index < count; index++) { + Append(kQuote); + AppendUTF16toUTF8(stores[index], *this); + Append(kQuote); + + if (index != count - 1) { + Append(kCommaSpace); + } + } + + Append(kCloseBracket); + Append(kCommaSpace); + + switch (aTransaction->GetMode()) { + case IDBTransaction::READ_ONLY: + AppendLiteral("\"readonly\""); + break; + case IDBTransaction::READ_WRITE: + AppendLiteral("\"readwrite\""); + break; + case IDBTransaction::READ_WRITE_FLUSH: + AppendLiteral("\"readwriteflush\""); + break; + case IDBTransaction::CLEANUP: + AppendLiteral("\"cleanup\""); + break; + case IDBTransaction::VERSION_CHANGE: + AppendLiteral("\"versionchange\""); + break; + default: + MOZ_CRASH("Unknown mode!"); + }; + } + + explicit + LoggingString(IDBObjectStore* aObjectStore) + : nsAutoCString(kQuote) + { + MOZ_ASSERT(aObjectStore); + + AppendUTF16toUTF8(aObjectStore->Name(), *this); + Append(kQuote); + } + + explicit + LoggingString(IDBIndex* aIndex) + : nsAutoCString(kQuote) + { + MOZ_ASSERT(aIndex); + + AppendUTF16toUTF8(aIndex->Name(), *this); + Append(kQuote); + } + + explicit + LoggingString(IDBKeyRange* aKeyRange) + { + if (aKeyRange) { + if (aKeyRange->IsOnly()) { + Assign(LoggingString(aKeyRange->Lower())); + } else { + if (aKeyRange->LowerOpen()) { + Assign(kOpenParen); + } else { + Assign(kOpenBracket); + } + + Append(LoggingString(aKeyRange->Lower())); + AppendLiteral(", "); + Append(LoggingString(aKeyRange->Upper())); + + if (aKeyRange->UpperOpen()) { + Append(kCloseParen); + } else { + Append(kCloseBracket); + } + } + } else { + AssignLiteral("<undefined>"); + } + } + + explicit + LoggingString(const Key& aKey) + { + if (aKey.IsUnset()) { + AssignLiteral("<undefined>"); + } else if (aKey.IsFloat()) { + AppendPrintf("%g", aKey.ToFloat()); + } else if (aKey.IsDate()) { + AppendPrintf("<Date %g>", aKey.ToDateMsec()); + } else if (aKey.IsString()) { + nsAutoString str; + aKey.ToString(str); + AppendPrintf("\"%s\"", NS_ConvertUTF16toUTF8(str).get()); + } else if (aKey.IsBinary()) { + AssignLiteral("[object ArrayBuffer]"); + } else { + MOZ_ASSERT(aKey.IsArray()); + AssignLiteral("[...]"); + } + } + + explicit + LoggingString(const IDBCursor::Direction aDirection) + { + switch (aDirection) { + case IDBCursor::NEXT: + AssignLiteral("\"next\""); + break; + case IDBCursor::NEXT_UNIQUE: + AssignLiteral("\"nextunique\""); + break; + case IDBCursor::PREV: + AssignLiteral("\"prev\""); + break; + case IDBCursor::PREV_UNIQUE: + AssignLiteral("\"prevunique\""); + break; + default: + MOZ_CRASH("Unknown direction!"); + }; + } + + explicit + LoggingString(const Optional<uint64_t>& aVersion) + { + if (aVersion.WasPassed()) { + AppendInt(aVersion.Value()); + } else { + AssignLiteral("<undefined>"); + } + } + + explicit + LoggingString(const Optional<uint32_t>& aLimit) + { + if (aLimit.WasPassed()) { + AppendInt(aLimit.Value()); + } else { + AssignLiteral("<undefined>"); + } + } + + LoggingString(IDBObjectStore* aObjectStore, const Key& aKey) + { + MOZ_ASSERT(aObjectStore); + + if (!aObjectStore->HasValidKeyPath()) { + Append(LoggingString(aKey)); + } + } + + LoggingString(nsIDOMEvent* aEvent, const char16_t* aDefault) + : nsAutoCString(kQuote) + { + MOZ_ASSERT(aDefault); + + nsString eventType; + + if (aEvent) { + MOZ_ALWAYS_SUCCEEDS(aEvent->GetType(eventType)); + } else { + eventType = nsDependentString(aDefault); + } + + AppendUTF16toUTF8(eventType, *this); + Append(kQuote); + } +}; + +inline void +LoggingHelper(bool aUseProfiler, const char* aFmt, ...) +{ + MOZ_ASSERT(IndexedDatabaseManager::GetLoggingMode() != + IndexedDatabaseManager::Logging_Disabled); + MOZ_ASSERT(aFmt); + + mozilla::LogModule* logModule = IndexedDatabaseManager::GetLoggingModule(); + MOZ_ASSERT(logModule); + + static const mozilla::LogLevel logLevel = LogLevel::Warning; + + if (MOZ_LOG_TEST(logModule, logLevel) || + (aUseProfiler && profiler_is_active())) { + nsAutoCString message; + + { + va_list args; + va_start(args, aFmt); + + message.AppendPrintf(aFmt, args); + + va_end(args); + } + + MOZ_LOG(logModule, logLevel, ("%s", message.get())); + + if (aUseProfiler) { + PROFILER_MARKER(message.get()); + } + } +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#define IDB_LOG_MARK(_detailedFmt, _conciseFmt, ...) \ + do { \ + using namespace mozilla::dom::indexedDB; \ + \ + const IndexedDatabaseManager::LoggingMode mode = \ + IndexedDatabaseManager::GetLoggingMode(); \ + \ + if (mode != IndexedDatabaseManager::Logging_Disabled) { \ + const char* _fmt; \ + if (mode == IndexedDatabaseManager::Logging_Concise || \ + mode == IndexedDatabaseManager::Logging_ConciseProfilerMarks) { \ + _fmt = _conciseFmt; \ + } else { \ + MOZ_ASSERT( \ + mode == IndexedDatabaseManager::Logging_Detailed || \ + mode == IndexedDatabaseManager::Logging_DetailedProfilerMarks); \ + _fmt = _detailedFmt; \ + } \ + \ + const bool _useProfiler = \ + mode == IndexedDatabaseManager::Logging_ConciseProfilerMarks || \ + mode == IndexedDatabaseManager::Logging_DetailedProfilerMarks; \ + \ + LoggingHelper(_useProfiler, _fmt, ##__VA_ARGS__); \ + } \ + } while (0) + +#define IDB_LOG_ID_STRING(...) \ + mozilla::dom::indexedDB::LoggingIdString(__VA_ARGS__).get() + +#define IDB_LOG_STRINGIFY(...) \ + mozilla::dom::indexedDB::LoggingString(__VA_ARGS__).get() + +#endif // mozilla_dom_indexeddb_profilerhelpers_h__ diff --git a/dom/indexedDB/ReportInternalError.cpp b/dom/indexedDB/ReportInternalError.cpp new file mode 100644 index 000000000..267b84a76 --- /dev/null +++ b/dom/indexedDB/ReportInternalError.cpp @@ -0,0 +1,36 @@ +/* -*- 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 "ReportInternalError.h" + +#include "mozilla/IntegerPrintfMacros.h" + +#include "nsContentUtils.h" +#include "nsPrintfCString.h" + +namespace mozilla { +namespace dom { +namespace indexedDB { + +void +ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) +{ + // Get leaf of file path + for (const char* p = aFile; *p; ++p) { + if (*p == '/' && *(p + 1)) { + aFile = p + 1; + } + } + + nsContentUtils::LogSimpleConsoleError( + NS_ConvertUTF8toUTF16(nsPrintfCString( + "IndexedDB %s: %s:%lu", aStr, aFile, aLine)), + "indexedDB"); +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/ReportInternalError.h b/dom/indexedDB/ReportInternalError.h new file mode 100644 index 000000000..f29af1c5f --- /dev/null +++ b/dom/indexedDB/ReportInternalError.h @@ -0,0 +1,58 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_reportinternalerror_h__ +#define mozilla_dom_indexeddb_reportinternalerror_h__ + +#include "nsDebug.h" + +#include "IndexedDatabase.h" + +#define IDB_WARNING(...) \ + do { \ + nsPrintfCString s(__VA_ARGS__); \ + mozilla::dom::indexedDB::ReportInternalError(__FILE__, __LINE__, s.get()); \ + NS_WARNING(s.get()); \ + } while (0) + +#define IDB_REPORT_INTERNAL_ERR() \ + mozilla::dom::indexedDB::ReportInternalError(__FILE__, __LINE__, \ + "UnknownErr") + +// Based on NS_ENSURE_TRUE +#define IDB_ENSURE_TRUE(x, ret) \ + do { \ + if (MOZ_UNLIKELY(!(x))) { \ + IDB_REPORT_INTERNAL_ERR(); \ + NS_WARNING("IDB_ENSURE_TRUE(" #x ") failed"); \ + return ret; \ + } \ + } while(0) + +// Based on NS_ENSURE_SUCCESS +#define IDB_ENSURE_SUCCESS(res, ret) \ + do { \ + nsresult __rv = res; /* Don't evaluate |res| more than once */ \ + if (NS_FAILED(__rv)) { \ + IDB_REPORT_INTERNAL_ERR(); \ + NS_ENSURE_SUCCESS_BODY(res, ret) \ + return ret; \ + } \ + } while(0) + + +namespace mozilla { +namespace dom { +namespace indexedDB { + +void +ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr); + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_reportinternalerror_h__ diff --git a/dom/indexedDB/ScriptErrorHelper.cpp b/dom/indexedDB/ScriptErrorHelper.cpp new file mode 100644 index 000000000..2db9c50e3 --- /dev/null +++ b/dom/indexedDB/ScriptErrorHelper.cpp @@ -0,0 +1,249 @@ +/* -*- 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 "ScriptErrorHelper.h" + +#include "MainThreadUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace { + +class ScriptErrorRunnable final : public mozilla::Runnable +{ + nsString mMessage; + nsCString mMessageName; + nsString mFilename; + uint32_t mLineNumber; + uint32_t mColumnNumber; + uint32_t mSeverityFlag; + uint64_t mInnerWindowID; + bool mIsChrome; + +public: + ScriptErrorRunnable(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + : mMessage(aMessage) + , mFilename(aFilename) + , mLineNumber(aLineNumber) + , mColumnNumber(aColumnNumber) + , mSeverityFlag(aSeverityFlag) + , mInnerWindowID(aInnerWindowID) + , mIsChrome(aIsChrome) + { + MOZ_ASSERT(!NS_IsMainThread()); + mMessageName.SetIsVoid(true); + } + + ScriptErrorRunnable(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + : mMessageName(aMessageName) + , mFilename(aFilename) + , mLineNumber(aLineNumber) + , mColumnNumber(aColumnNumber) + , mSeverityFlag(aSeverityFlag) + , mInnerWindowID(aInnerWindowID) + , mIsChrome(aIsChrome) + { + MOZ_ASSERT(!NS_IsMainThread()); + mMessage.SetIsVoid(true); + } + + static void + DumpLocalizedMessage(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aMessageName.IsEmpty()); + + nsXPIDLString localizedMessage; + if (NS_WARN_IF(NS_FAILED( + nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, + aMessageName.BeginReading(), + localizedMessage)))) { + return; + } + + Dump(localizedMessage, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + } + + static void + Dump(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString category; + if (aIsChrome) { + category.AssignLiteral("chrome "); + } else { + category.AssignLiteral("content "); + } + category.AppendLiteral("javascript"); + + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + MOZ_ASSERT(consoleService); + + nsCOMPtr<nsIScriptError> scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + MOZ_ASSERT(scriptError); + + if (aInnerWindowID) { + MOZ_ALWAYS_SUCCEEDS( + scriptError->InitWithWindowID(aMessage, + aFilename, + /* aSourceLine */ EmptyString(), + aLineNumber, + aColumnNumber, + aSeverityFlag, + category, + aInnerWindowID)); + } else { + MOZ_ALWAYS_SUCCEEDS( + scriptError->Init(aMessage, + aFilename, + /* aSourceLine */ EmptyString(), + aLineNumber, + aColumnNumber, + aSeverityFlag, + category.get())); + } + + MOZ_ALWAYS_SUCCEEDS(consoleService->LogMessage(scriptError)); + } + + NS_IMETHOD + Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mMessage.IsVoid() != mMessageName.IsVoid()); + + if (!mMessage.IsVoid()) { + Dump(mMessage, + mFilename, + mLineNumber, + mColumnNumber, + mSeverityFlag, + mIsChrome, + mInnerWindowID); + return NS_OK; + } + + DumpLocalizedMessage(mMessageName, + mFilename, + mLineNumber, + mColumnNumber, + mSeverityFlag, + mIsChrome, + mInnerWindowID); + + return NS_OK; + } + +private: + virtual ~ScriptErrorRunnable() {} +}; + +} // namespace + +namespace mozilla { +namespace dom { +namespace indexedDB { + +/*static*/ void +ScriptErrorHelper::Dump(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) +{ + if (NS_IsMainThread()) { + ScriptErrorRunnable::Dump(aMessage, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + } else { + RefPtr<ScriptErrorRunnable> runnable = + new ScriptErrorRunnable(aMessage, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + } +} + +/*static*/ void +ScriptErrorHelper::DumpLocalizedMessage(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) +{ + if (NS_IsMainThread()) { + ScriptErrorRunnable::DumpLocalizedMessage(aMessageName, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + } else { + RefPtr<ScriptErrorRunnable> runnable = + new ScriptErrorRunnable(aMessageName, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + } +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/ScriptErrorHelper.h b/dom/indexedDB/ScriptErrorHelper.h new file mode 100644 index 000000000..d1cd8c749 --- /dev/null +++ b/dom/indexedDB/ScriptErrorHelper.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_scripterrorhelper_h__ +#define mozilla_dom_indexeddb_scripterrorhelper_h__ + +class nsAString; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +// Helper to report a script error to the main thread. +class ScriptErrorHelper +{ +public: + static void Dump(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, /* nsIScriptError::xxxFlag */ + bool aIsChrome, + uint64_t aInnerWindowID); + + static void DumpLocalizedMessage(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, /* nsIScriptError::xxxFlag */ + bool aIsChrome, + uint64_t aInnerWindowID); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_scripterrorhelper_h__
\ No newline at end of file diff --git a/dom/indexedDB/SerializationHelpers.h b/dom/indexedDB/SerializationHelpers.h new file mode 100644 index 000000000..37e8c40ab --- /dev/null +++ b/dom/indexedDB/SerializationHelpers.h @@ -0,0 +1,96 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_indexeddb_serializationhelpers_h__ +#define mozilla_dom_indexeddb_serializationhelpers_h__ + +#include "ipc/IPCMessageUtils.h" + +#include "mozilla/dom/indexedDB/Key.h" +#include "mozilla/dom/indexedDB/KeyPath.h" +#include "mozilla/dom/IDBCursor.h" +#include "mozilla/dom/IDBTransaction.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::indexedDB::StructuredCloneFile::FileType> : + public ContiguousEnumSerializer< + mozilla::dom::indexedDB::StructuredCloneFile::FileType, + mozilla::dom::indexedDB::StructuredCloneFile::eBlob, + mozilla::dom::indexedDB::StructuredCloneFile::eEndGuard> +{ }; + +template <> +struct ParamTraits<mozilla::dom::indexedDB::Key> +{ + typedef mozilla::dom::indexedDB::Key paramType; + + static void Write(Message* aMsg, const paramType& aParam) + { + WriteParam(aMsg, aParam.mBuffer); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) + { + return ReadParam(aMsg, aIter, &aResult->mBuffer); + } + + static void Log(const paramType& aParam, std::wstring* aLog) + { + LogParam(aParam.mBuffer, aLog); + } +}; + +template <> +struct ParamTraits<mozilla::dom::indexedDB::KeyPath::KeyPathType> : + public ContiguousEnumSerializer<mozilla::dom::indexedDB::KeyPath::KeyPathType, + mozilla::dom::indexedDB::KeyPath::NONEXISTENT, + mozilla::dom::indexedDB::KeyPath::ENDGUARD> +{ }; + +template <> +struct ParamTraits<mozilla::dom::indexedDB::KeyPath> +{ + typedef mozilla::dom::indexedDB::KeyPath paramType; + + static void Write(Message* aMsg, const paramType& aParam) + { + WriteParam(aMsg, aParam.mType); + WriteParam(aMsg, aParam.mStrings); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) + { + return ReadParam(aMsg, aIter, &aResult->mType) && + ReadParam(aMsg, aIter, &aResult->mStrings); + } + + static void Log(const paramType& aParam, std::wstring* aLog) + { + LogParam(aParam.mStrings, aLog); + } +}; + +template <> +struct ParamTraits<mozilla::dom::IDBCursor::Direction> : + public ContiguousEnumSerializer< + mozilla::dom::IDBCursor::Direction, + mozilla::dom::IDBCursor::NEXT, + mozilla::dom::IDBCursor::DIRECTION_INVALID> +{ }; + +template <> +struct ParamTraits<mozilla::dom::IDBTransaction::Mode> : + public ContiguousEnumSerializer< + mozilla::dom::IDBTransaction::Mode, + mozilla::dom::IDBTransaction::READ_ONLY, + mozilla::dom::IDBTransaction::MODE_INVALID> +{ }; + +} // namespace IPC + +#endif // mozilla_dom_indexeddb_serializationhelpers_h__ diff --git a/dom/indexedDB/crashtests/726376-1.html b/dom/indexedDB/crashtests/726376-1.html new file mode 100644 index 000000000..4187678fa --- /dev/null +++ b/dom/indexedDB/crashtests/726376-1.html @@ -0,0 +1,7 @@ +<script> + +var a = []; +a[0] = a; +indexedDB.cmp.bind(indexedDB)(a, a); + +</script> diff --git a/dom/indexedDB/crashtests/crashtests.list b/dom/indexedDB/crashtests/crashtests.list new file mode 100644 index 000000000..69f5dab0b --- /dev/null +++ b/dom/indexedDB/crashtests/crashtests.list @@ -0,0 +1 @@ +load 726376-1.html diff --git a/dom/indexedDB/moz.build b/dom/indexedDB/moz.build new file mode 100644 index 000000000..1fb01135c --- /dev/null +++ b/dom/indexedDB/moz.build @@ -0,0 +1,111 @@ +# -*- 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/. + +TEST_DIRS += ['test/extensions'] + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini'] + +XPCSHELL_TESTS_MANIFESTS += [ + 'test/unit/xpcshell-child-process.ini', + 'test/unit/xpcshell-parent-process.ini' +] + +if CONFIG['ENABLE_INTL_API']: + MOCHITEST_MANIFESTS += ['test/mochitest-intl-api.ini'] + +EXPORTS.mozilla.dom += [ + 'IDBCursor.h', + 'IDBDatabase.h', + 'IDBEvents.h', + 'IDBFactory.h', + 'IDBFileHandle.h', + 'IDBFileRequest.h', + 'IDBIndex.h', + 'IDBKeyRange.h', + 'IDBMutableFile.h', + 'IDBObjectStore.h', + 'IDBRequest.h', + 'IDBTransaction.h', + 'IDBWrapperCache.h', + 'IndexedDatabase.h', + 'IndexedDatabaseManager.h', +] + +EXPORTS.mozilla.dom.indexedDB += [ + 'ActorsParent.h', + 'FileSnapshot.h', + 'Key.h', + 'KeyPath.h', + 'SerializationHelpers.h', +] + +UNIFIED_SOURCES += [ + 'ActorsChild.cpp', + 'FileInfo.cpp', + 'FileSnapshot.cpp', + 'IDBCursor.cpp', + 'IDBDatabase.cpp', + 'IDBEvents.cpp', + 'IDBFactory.cpp', + 'IDBFileHandle.cpp', + 'IDBFileRequest.cpp', + 'IDBIndex.cpp', + 'IDBKeyRange.cpp', + 'IDBMutableFile.cpp', + 'IDBObjectStore.cpp', + 'IDBRequest.cpp', + 'IDBTransaction.cpp', + 'IDBWrapperCache.cpp', + 'IndexedDatabaseManager.cpp', + 'KeyPath.cpp', + 'PermissionRequestBase.cpp', + 'ReportInternalError.cpp', + 'ScriptErrorHelper.cpp', +] + +SOURCES += [ + 'ActorsParent.cpp', # This file is huge. + 'Key.cpp', # We disable a warning on this file only +] + +IPDL_SOURCES += [ + 'PBackgroundIDBCursor.ipdl', + 'PBackgroundIDBDatabase.ipdl', + 'PBackgroundIDBDatabaseFile.ipdl', + 'PBackgroundIDBDatabaseRequest.ipdl', + 'PBackgroundIDBFactory.ipdl', + 'PBackgroundIDBFactoryRequest.ipdl', + 'PBackgroundIDBRequest.ipdl', + 'PBackgroundIDBSharedTypes.ipdlh', + 'PBackgroundIDBTransaction.ipdl', + 'PBackgroundIDBVersionChangeTransaction.ipdl', + 'PBackgroundIndexedDBUtils.ipdl', + 'PIndexedDBPermissionRequest.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CC']: + # Suppress gcc warning about a comparison being always false due to the + # range of the data type + SOURCES['Key.cpp'].flags += ['-Wno-error=type-limits'] + CXXFLAGS += ['-Wno-error=shadow'] + +LOCAL_INCLUDES += [ + '/db/sqlite3/src', + '/dom/base', + '/dom/storage', + '/dom/workers', + '/ipc/glue', + '/xpcom/build', + '/xpcom/threads', +] diff --git a/dom/indexedDB/test/bfcache_iframe1.html b/dom/indexedDB/test/bfcache_iframe1.html new file mode 100644 index 000000000..ade5dc555 --- /dev/null +++ b/dom/indexedDB/test/bfcache_iframe1.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <script> + var request = indexedDB.open(parent.location, 1); + request.onupgradeneeded = function(e) { + var db = e.target.result; + // This should never be called + db.onversionchange = function(e) { + db.transaction(["mystore"]).objectStore("mystore").put({ hello: "fail" }, 42); + } + var trans = e.target.transaction; + if (db.objectStoreNames.contains("mystore")) { + db.deleteObjectStore("mystore"); + } + var store = db.createObjectStore("mystore"); + store.add({ hello: "world" }, 42); + trans.oncomplete = function() { + parent.postMessage("go", "http://mochi.test:8888"); + } + }; + </script> +</head> +<body> + This is page one. +</body> +</html> diff --git a/dom/indexedDB/test/bfcache_iframe2.html b/dom/indexedDB/test/bfcache_iframe2.html new file mode 100644 index 000000000..43cb92a58 --- /dev/null +++ b/dom/indexedDB/test/bfcache_iframe2.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> + <script> + var res = {}; + var request = indexedDB.open(parent.location, 2); + request.onblocked = function() { + res.blockedFired = true; + } + request.onupgradeneeded = function(e) { + var db = e.target.result; + res.version = db.version; + res.storeCount = db.objectStoreNames.length; + + var trans = request.transaction; + trans.objectStore("mystore").get(42).onsuccess = function(e) { + res.value = JSON.stringify(e.target.result); + } + trans.oncomplete = function() { + parent.postMessage(JSON.stringify(res), "http://mochi.test:8888"); + } + }; + + </script> +</head> +<body> + This is page two. +</body> +</html> diff --git a/dom/indexedDB/test/blob_worker_crash_iframe.html b/dom/indexedDB/test/blob_worker_crash_iframe.html new file mode 100644 index 000000000..304b87ea0 --- /dev/null +++ b/dom/indexedDB/test/blob_worker_crash_iframe.html @@ -0,0 +1,98 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript"> + function report(result) { + var message = { source: "iframe" }; + message.result = result; + window.parent.postMessage(message, "*"); + } + + function runIndexedDBTest() { + var db = null; + + // Create the data-store + function createDatastore() { + try { + var request = indexedDB.open(window.location.pathname, 1); + request.onupgradeneeded = function(event) { + event.target.result.createObjectStore("foo"); + } + request.onsuccess = function(event) { + db = event.target.result; + createAndStoreBlob(); + } + } + catch (e) { +dump("EXCEPTION IN CREATION: " + e + "\n " + e.stack + "\n"); + report(false); + } + } + + function createAndStoreBlob() { + const BLOB_DATA = ["fun ", "times ", "all ", "around!"]; + var blob = new Blob(BLOB_DATA, { type: "text/plain" }); + var objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add({ blob: blob }, 42).onsuccess = refetchBlob; + } + + function refetchBlob() { + var foo = db.transaction("foo").objectStore("foo"); + foo.get(42).onsuccess = fetchedBlobCreateWorkerAndSendBlob; + } + + function fetchedBlobCreateWorkerAndSendBlob(event) { + var idbBlob = event.target.result.blob; + var compositeBlob = new Blob(['I like the following blob: ', idbBlob], + { type: "text/fancy" }); + + function workerScript() { + onmessage = function(event) { + // Save the Blob to the worker's global scope. + self.holdOntoBlob = event.data; + // Send any message so we can serialize and keep our runtime behaviour + // consistent. + postMessage('kung fu death grip established'); + } + } + + var url = + URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + // Keep a reference to the worker on the window. + var worker = window.worker = new Worker(url); + worker.postMessage(compositeBlob); + worker.onmessage = workerLatchedBlobDeleteFromDB; + } + + function workerLatchedBlobDeleteFromDB() { + // Delete the reference to the Blob from the database leaving the worker + // thread reference as the only live reference once a GC has cleaned + // out our references that we sent to the worker. The page that owns + // us triggers a GC just for that reason. + var objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.delete(42).onsuccess = closeDBTellOwningThread; + } + + function closeDBTellOwningThread(event) { + // Now that worker has latched the blob, clean up the database. + db.close(); + db = null; + report('ready'); + } + + createDatastore(); + } + </script> + +</head> + +<body onload="runIndexedDBTest();"> +</body> + +</html> diff --git a/dom/indexedDB/test/browser.ini b/dom/indexedDB/test/browser.ini new file mode 100644 index 000000000..85671568d --- /dev/null +++ b/dom/indexedDB/test/browser.ini @@ -0,0 +1,21 @@ +[DEFAULT] +skip-if = (buildapp != "browser") +support-files = + head.js + browser_forgetThisSiteAdd.html + browser_forgetThisSiteGet.html + browserHelpers.js + browser_permissionsPrompt.html + browser_permissionsSharedWorker.html + browser_permissionsSharedWorker.js + browser_permissionsWorker.html + browser_permissionsWorker.js + bug839193.js + bug839193.xul + +[browser_forgetThisSite.js] +[browser_permissionsPromptAllow.js] +[browser_permissionsPromptDeny.js] +[browser_permissionsPromptWorker.js] +[browser_perwindow_privateBrowsing.js] +[browser_bug839193.js] diff --git a/dom/indexedDB/test/browserHelpers.js b/dom/indexedDB/test/browserHelpers.js new file mode 100644 index 000000000..c61c78943 --- /dev/null +++ b/dom/indexedDB/test/browserHelpers.js @@ -0,0 +1,56 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +var testResult; +var testException; + +function runTest() +{ + testGenerator.next(); +} + +function finishTestNow() +{ + if (testGenerator) { + testGenerator.close(); + testGenerator = undefined; + } +} + +function finishTest() +{ + setTimeout(finishTestNow, 0); + setTimeout(() => { + if (window.testFinishedCallback) + window.testFinishedCallback(testResult, testException); + else { + let message; + if (testResult) + message = "ok"; + else + message = testException; + window.parent.postMessage(message, "*"); + } + }, 0); +} + +function grabEventAndContinueHandler(event) +{ + testGenerator.send(event); +} + +function errorHandler(event) +{ + throw new Error("indexedDB error, code " + event.target.error.name); +} + +function continueToNextStep() +{ + SimpleTest.executeSoon(function() { + testGenerator.next(); + }); +} diff --git a/dom/indexedDB/test/browser_bug839193.js b/dom/indexedDB/test/browser_bug839193.js new file mode 100644 index 000000000..eef284794 --- /dev/null +++ b/dom/indexedDB/test/browser_bug839193.js @@ -0,0 +1,41 @@ +/* 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/. */ + +var gTestRoot = getRootDirectory(gTestPath); +var gBugWindow = null; +var gIterations = 5; + +function onLoad() { + gBugWindow.close(); +} + +function onUnload() { + if (!gIterations) { + gBugWindow = null; + Services.obs.removeObserver(onLoad, "bug839193-loaded"); + Services.obs.removeObserver(onUnload, "bug839193-unloaded"); + + window.focus(); + finish(); + } else { + gBugWindow = window.openDialog(gTestRoot + "bug839193.xul"); + gIterations--; + } +} + +// This test is about leaks, which are handled by the test harness, so +// there are no actual checks here. Whether or not this test passes or fails +// will be apparent by the checks the harness performs. +function test() { + waitForExplicitFinish(); + + // This test relies on the test timing out in order to indicate failure so + // let's add a dummy pass. + ok(true, "Each test requires at least one pass, fail or todo so here is a pass."); + + Services.obs.addObserver(onLoad, "bug839193-loaded", false); + Services.obs.addObserver(onUnload, "bug839193-unloaded", false); + + gBugWindow = window.openDialog(gTestRoot + "bug839193.xul"); +} diff --git a/dom/indexedDB/test/browser_forgetThisSite.js b/dom/indexedDB/test/browser_forgetThisSite.js new file mode 100644 index 000000000..c1177908f --- /dev/null +++ b/dom/indexedDB/test/browser_forgetThisSite.js @@ -0,0 +1,109 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm"); + +const domains = [ + "mochi.test:8888", + "www.example.com" +]; + +const addPath = "/browser/dom/indexedDB/test/browser_forgetThisSiteAdd.html"; +const getPath = "/browser/dom/indexedDB/test/browser_forgetThisSiteGet.html"; + +const testPageURL1 = "http://" + domains[0] + addPath; +const testPageURL2 = "http://" + domains[1] + addPath; +const testPageURL3 = "http://" + domains[0] + getPath; +const testPageURL4 = "http://" + domains[1] + getPath; + +function test() +{ + requestLongerTimeout(2); + waitForExplicitFinish(); + // Avoids the prompt + setPermission(testPageURL1, "indexedDB"); + setPermission(testPageURL2, "indexedDB"); + executeSoon(test1); +} + +function test1() +{ + // Set database version for domain 1 + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(result, exception) { + ok(result == 11, "Set version on database in " + testPageURL1); + ok(!exception, "No exception"); + gBrowser.removeCurrentTab(); + + executeSoon(test2); + }); + }, true); + content.location = testPageURL1; +} + +function test2() +{ + // Set database version for domain 2 + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(result, exception) { + ok(result == 11, "Set version on database in " + testPageURL2); + ok(!exception, "No exception"); + gBrowser.removeCurrentTab(); + + executeSoon(test3); + }); + }, true); + content.location = testPageURL2; +} + +function test3() +{ + // Remove database from domain 2 + ForgetAboutSite.removeDataFromDomain(domains[1]); + setPermission(testPageURL4, "indexedDB"); + executeSoon(test4); +} + +function test4() +{ + // Get database version for domain 1 + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(result, exception) { + ok(result == 11, "Got correct version on database in " + testPageURL3); + ok(!exception, "No exception"); + gBrowser.removeCurrentTab(); + + executeSoon(test5); + }); + }, true); + content.location = testPageURL3; +} + +function test5() +{ + // Get database version for domain 2 + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(result, exception) { + ok(result == 1, "Got correct version on database in " + testPageURL4); + ok(!exception, "No exception"); + gBrowser.removeCurrentTab(); + + executeSoon(finish); + }); + }, true); + content.location = testPageURL4; +} diff --git a/dom/indexedDB/test/browser_forgetThisSiteAdd.html b/dom/indexedDB/test/browser_forgetThisSiteAdd.html new file mode 100644 index 000000000..2982012a6 --- /dev/null +++ b/dom/indexedDB/test/browser_forgetThisSiteAdd.html @@ -0,0 +1,39 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + let request = indexedDB.open("browser_forgetThisSite.js", 11); + request.onerror = grabEventAndContinueHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + if (event.type == "error") { + testException = event.target.error.name; + } + else { + let db = event.target.result; + + testResult = db.version; + + event.target.transaction.oncomplete = finishTest; + yield undefined; + } + + yield undefined; + } + </script> + + <script type="text/javascript;version=1.7" src="browserHelpers.js"></script> + + </head> + + <body onload="runTest();" onunload="finishTestNow();"></body> + +</html> diff --git a/dom/indexedDB/test/browser_forgetThisSiteGet.html b/dom/indexedDB/test/browser_forgetThisSiteGet.html new file mode 100644 index 000000000..948eaa68f --- /dev/null +++ b/dom/indexedDB/test/browser_forgetThisSiteGet.html @@ -0,0 +1,36 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + let request = indexedDB.open("browser_forgetThisSite.js"); + request.onerror = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + if (event.type == "error") { + testException = event.target.error.name; + } + else { + let db = event.target.result; + testResult = db.version; + } + + finishTest() + yield undefined; + } + </script> + + <script type="text/javascript;version=1.7" src="browserHelpers.js"></script> + + </head> + + <body onload="runTest();" onunload="finishTestNow();"></body> + +</html> diff --git a/dom/indexedDB/test/browser_permissionsPrompt.html b/dom/indexedDB/test/browser_permissionsPrompt.html new file mode 100644 index 000000000..0ee698b97 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsPrompt.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <meta charset=UTF-8> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, { version: 1, + storage: "persistent" }); + request.onerror = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + if (event.type == "success") { + testResult = event.target.result instanceof IDBDatabase; + } + else { + testException = event.target.error.name; + } + + event.preventDefault(); + + finishTest() + yield undefined; + } + </script> + + <script type="text/javascript;version=1.7" src="browserHelpers.js"></script> + + </head> + + <body onload="runTest();" onunload="finishTestNow();"></body> + +</html> diff --git a/dom/indexedDB/test/browser_permissionsPromptAllow.js b/dom/indexedDB/test/browser_permissionsPromptAllow.js new file mode 100644 index 000000000..dd0921872 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsPromptAllow.js @@ -0,0 +1,90 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testPageURL = "http://mochi.test:8888/browser/" + + "dom/indexedDB/test/browser_permissionsPrompt.html"; +const notificationID = "indexedDB-permissions-prompt"; + +function test() +{ + waitForExplicitFinish(); + + // We want a prompt. + removePermission(testPageURL, "indexedDB"); + executeSoon(test1); +} + +function test1() +{ + info("creating tab"); + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(isIDBDatabase, exception) { + ok(isIDBDatabase, + "First database creation was successful"); + ok(!exception, "No exception"); + is(getPermission(testPageURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.ALLOW_ACTION, + "Correct permission set"); + gBrowser.removeCurrentTab(); + executeSoon(test2); + }); + + registerPopupEventHandler("popupshowing", function () { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function () { + ok(true, "prompt shown"); + triggerMainCommand(this); + }); + registerPopupEventHandler("popuphidden", function () { + ok(true, "prompt hidden"); + }); + + }, true); + + info("loading test page: " + testPageURL); + content.location = testPageURL; +} + +function test2() +{ + info("creating tab"); + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(isIDBDatabase, exception) { + ok(isIDBDatabase, + "First database creation was successful"); + ok(!exception, "No exception"); + is(getPermission(testPageURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.ALLOW_ACTION, + "Correct permission set"); + gBrowser.removeCurrentTab(); + unregisterAllPopupEventHandlers(); + removePermission(testPageURL, "indexedDB"); + executeSoon(finish); + }); + + registerPopupEventHandler("popupshowing", function () { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popupshown", function () { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popuphidden", function () { + ok(false, "Shouldn't show a popup this time"); + }); + + }, true); + + info("loading test page: " + testPageURL); + content.location = testPageURL; +} diff --git a/dom/indexedDB/test/browser_permissionsPromptDeny.js b/dom/indexedDB/test/browser_permissionsPromptDeny.js new file mode 100644 index 000000000..e7132e004 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsPromptDeny.js @@ -0,0 +1,110 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testPageURL = "http://mochi.test:8888/browser/" + + "dom/indexedDB/test/browser_permissionsPrompt.html"; +const notificationID = "indexedDB-permissions-prompt"; + +function promiseMessage(aMessage, browser) { + return ContentTask.spawn(browser.selectedBrowser, aMessage, function* (aMessage) { + yield new Promise((resolve, reject) => { + content.addEventListener("message", function messageListener(event) { + content.removeEventListener("message", messageListener); + is(event.data, aMessage, "received " + aMessage); + if (event.data == aMessage) + resolve(); + else + reject(); + }); + }); + }); +} + +add_task(function test1() { + removePermission(testPageURL, "indexedDB"); + + info("creating tab"); + gBrowser.selectedTab = gBrowser.addTab(); + + info("loading test page: " + testPageURL); + gBrowser.selectedBrowser.loadURI(testPageURL); + yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + registerPopupEventHandler("popupshowing", function () { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function () { + ok(true, "prompt shown"); + triggerSecondaryCommand(this, 0); + }); + registerPopupEventHandler("popuphidden", function () { + ok(true, "prompt hidden"); + }); + + yield promiseMessage("InvalidStateError", gBrowser); + + is(getPermission(testPageURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.DENY_ACTION, + "Correct permission set"); + gBrowser.removeCurrentTab(); +}); + +add_task(function test2() { + info("creating private window"); + let win = yield BrowserTestUtils.openNewBrowserWindow({ private : true }); + + info("creating private tab"); + win.gBrowser.selectedTab = win.gBrowser.addTab(); + + info("loading test page: " + testPageURL); + win.gBrowser.selectedBrowser.loadURI(testPageURL); + yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + + registerPopupEventHandler("popupshowing", function () { + ok(false, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function () { + ok(false, "prompt shown"); + }); + registerPopupEventHandler("popuphidden", function () { + ok(false, "prompt hidden"); + }); + yield promiseMessage("InvalidStateError", win.gBrowser); + + is(getPermission(testPageURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.DENY_ACTION, + "Correct permission set"); + unregisterAllPopupEventHandlers(); + win.gBrowser.removeCurrentTab(); + win.close(); +}); + +add_task(function test3() { + info("creating tab"); + gBrowser.selectedTab = gBrowser.addTab(); + + info("loading test page: " + testPageURL); + gBrowser.selectedBrowser.loadURI(testPageURL); + yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + registerPopupEventHandler("popupshowing", function () { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popupshown", function () { + ok(false, "Shouldn't show a popup this time"); + }); + registerPopupEventHandler("popuphidden", function () { + ok(false, "Shouldn't show a popup this time"); + }); + + yield promiseMessage("InvalidStateError", gBrowser); + + is(getPermission(testPageURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.DENY_ACTION, + "Correct permission set"); + gBrowser.removeCurrentTab(); + unregisterAllPopupEventHandlers(); + removePermission(testPageURL, "indexedDB"); +}); diff --git a/dom/indexedDB/test/browser_permissionsPromptWorker.js b/dom/indexedDB/test/browser_permissionsPromptWorker.js new file mode 100644 index 000000000..a60704e69 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsPromptWorker.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testWorkerURL = "http://mochi.test:8888/browser/" + + "dom/indexedDB/test/browser_permissionsWorker.html"; +const testSharedWorkerURL = "http://mochi.test:8888/browser/" + + "dom/indexedDB/test/browser_permissionsSharedWorker.html"; +const notificationID = "indexedDB-permissions-prompt"; + +function test() +{ + waitForExplicitFinish(); + executeSoon(test1); +} + +function test1() +{ + // We want a prompt. + removePermission(testWorkerURL, "indexedDB"); + + info("creating tab"); + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(isIDBDatabase, exception) { + ok(isIDBDatabase, "First database creation was successful"); + ok(!exception, "No exception"); + is(getPermission(testWorkerURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.ALLOW_ACTION, + "Correct permission set"); + gBrowser.removeCurrentTab(); + executeSoon(test2); + }); + + registerPopupEventHandler("popupshowing", function () { + ok(true, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function () { + ok(true, "prompt shown"); + triggerMainCommand(this); + }); + registerPopupEventHandler("popuphidden", function () { + ok(true, "prompt hidden"); + }); + + }, true); + + info("loading test page: " + testWorkerURL); + content.location = testWorkerURL; +} + +function test2() +{ + // We want a prompt. + removePermission(testSharedWorkerURL, "indexedDB"); + + info("creating tab"); + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(isIDBDatabase, exception) { + ok(!isIDBDatabase, "First database creation was successful"); + ok(exception, "No exception"); + is(getPermission(testSharedWorkerURL, "indexedDB"), + Components.interfaces.nsIPermissionManager.UNKNOWN_ACTION, + "Correct permission set"); + gBrowser.removeCurrentTab(); + executeSoon(finish); + }); + + registerPopupEventHandler("popupshowing", function () { + ok(false, "prompt showing"); + }); + registerPopupEventHandler("popupshown", function () { + ok(false, "prompt shown"); + }); + registerPopupEventHandler("popuphidden", function () { + ok(false, "prompt hidden"); + }); + + }, true); + + info("loading test page: " + testSharedWorkerURL); + content.location = testSharedWorkerURL; +} diff --git a/dom/indexedDB/test/browser_permissionsSharedWorker.html b/dom/indexedDB/test/browser_permissionsSharedWorker.html new file mode 100644 index 000000000..295f01fc5 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsSharedWorker.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + let testIsIDBDatabase; + let testException; + + function runTest() { + let w = new SharedWorker('browser_permissionsSharedWorker.js'); + w.port.onmessage = function(e) { + if (e.data.status == 'success') { + testIsIDBDatabase = e.data.isIDBDatabase; + } else { + testException = e.data.error; + } + + setTimeout(testFinishedCallback, 0, testIsIDBDatabase, testException); + } + + const name = window.location.pathname + "_sharedWorker"; + w.port.postMessage(name); + } + </script> + + </head> + + <body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/browser_permissionsSharedWorker.js b/dom/indexedDB/test/browser_permissionsSharedWorker.js new file mode 100644 index 000000000..59fe53326 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsSharedWorker.js @@ -0,0 +1,14 @@ +onconnect = function(e) { + e.ports[0].onmessage = function(e) { + var request = indexedDB.open(e.data, { version: 1, + storage: "persistent" }); + request.onsuccess = function(event) { + e.target.postMessage({ status: 'success', + isIDBDatabase: (event.target.result instanceof IDBDatabase) }); + } + + request.onerror = function(event) { + e.target.postMessage({ status: 'error', error: event.target.error.name }); + } + } +} diff --git a/dom/indexedDB/test/browser_permissionsWorker.html b/dom/indexedDB/test/browser_permissionsWorker.html new file mode 100644 index 000000000..c53adf975 --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsWorker.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + let testIsIDBDatabase; + let testException; + + function runTest() { + let w = new Worker('browser_permissionsWorker.js'); + w.onmessage = function(e) { + if (e.data.status == 'success') { + testIsIDBDatabase = e.data.isIDBDatabase; + } else { + testException = e.data.error; + } + + setTimeout(testFinishedCallback, 0, testIsIDBDatabase, testException); + } + + const name = window.location.pathname; + w.postMessage(name); + } + </script> + + </head> + + <body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/browser_permissionsWorker.js b/dom/indexedDB/test/browser_permissionsWorker.js new file mode 100644 index 000000000..3712acdeb --- /dev/null +++ b/dom/indexedDB/test/browser_permissionsWorker.js @@ -0,0 +1,12 @@ +onmessage = function(e) { + var request = indexedDB.open(e.data, { version: 1, + storage: "persistent" }); + request.onsuccess = function(event) { + postMessage({ status: 'success', + isIDBDatabase: (event.target.result instanceof IDBDatabase) }); + } + + request.onerror = function(event) { + postMessage({ status: 'error', error: event.target.error.name }); + } +} diff --git a/dom/indexedDB/test/browser_perwindow_privateBrowsing.js b/dom/indexedDB/test/browser_perwindow_privateBrowsing.js new file mode 100644 index 000000000..08d329cbc --- /dev/null +++ b/dom/indexedDB/test/browser_perwindow_privateBrowsing.js @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const testPageURL = "http://mochi.test:8888/browser/" + + "dom/indexedDB/test/browser_permissionsPrompt.html"; +const notificationID = "indexedDB-permissions-prompt"; + +function test() +{ + waitForExplicitFinish(); + // Avoids the actual prompt + setPermission(testPageURL, "indexedDB"); + executeSoon(test1); +} + +function test1() +{ + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + if (content.location != testPageURL) { + content.location = testPageURL; + return; + } + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(isIDBDatabase, exception) { + ok(isIDBDatabase, + "First database creation was successful"); + ok(!exception, "No exception"); + gBrowser.removeCurrentTab(); + + executeSoon(test2); + }); + }, true); + content.location = testPageURL; +} + +function test2() +{ + var win = OpenBrowserWindow({private: true}); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + executeSoon(() => test3(win)); + }, false); + registerCleanupFunction(() => win.close()); +} + +function test3(win) +{ + win.gBrowser.selectedTab = win.gBrowser.addTab(); + win.gBrowser.selectedBrowser.addEventListener("load", function () { + if (win.content.location != testPageURL) { + win.content.location = testPageURL; + return; + } + win.gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function(isIDBDatabase, exception) { + ok(!isIDBDatabase, "No database"); + is(exception, "InvalidStateError", "Correct exception"); + win.gBrowser.removeCurrentTab(); + + executeSoon(finish); + }, win); + }, true); + win.content.location = testPageURL; +} diff --git a/dom/indexedDB/test/bug839193.js b/dom/indexedDB/test/bug839193.js new file mode 100644 index 000000000..0982c5594 --- /dev/null +++ b/dom/indexedDB/test/bug839193.js @@ -0,0 +1,32 @@ +/* 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/. */ + +const nsIQuotaManagerService = Components.interfaces.nsIQuotaManagerService; + +var gURI = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService).newURI("http://localhost", null, null); + +function onUsageCallback(request) {} + +function onLoad() +{ + var quotaManagerService = + Components.classes["@mozilla.org/dom/quota-manager-service;1"] + .getService(nsIQuotaManagerService); + let principal = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager) + .createCodebasePrincipal(gURI, {}); + var quotaRequest = quotaManagerService.getUsageForPrincipal(principal, + onUsageCallback); + quotaRequest.cancel(); + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "bug839193-loaded", null); +} + +function onUnload() +{ + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "bug839193-unloaded", null); +} diff --git a/dom/indexedDB/test/bug839193.xul b/dom/indexedDB/test/bug839193.xul new file mode 100644 index 000000000..ccda48f95 --- /dev/null +++ b/dom/indexedDB/test/bug839193.xul @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<window id="main-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="Browser:bug839193" + onload="onLoad()" + onunload="onUnload()" + align="stretch" + screenX="10" screenY="10" + width="600" height="600" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript" src="chrome://mochitests/content/browser/dom/indexedDB/test/bug839193.js"/> +</window> diff --git a/dom/indexedDB/test/chrome.ini b/dom/indexedDB/test/chrome.ini new file mode 100644 index 000000000..a809b45b5 --- /dev/null +++ b/dom/indexedDB/test/chrome.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = chromeHelpers.js + +[test_globalObjects_chrome.xul] +[test_globalObjects_other.xul] diff --git a/dom/indexedDB/test/chromeHelpers.js b/dom/indexedDB/test/chromeHelpers.js new file mode 100644 index 000000000..16508f62a --- /dev/null +++ b/dom/indexedDB/test/chromeHelpers.js @@ -0,0 +1,42 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var { 'classes': Cc, 'interfaces': Ci, 'utils': Cu } = Components; + +var testGenerator = testSteps(); + +if (!window.runTest) { + window.runTest = function() + { + SimpleTest.waitForExplicitFinish(); + + testGenerator.next(); + } +} + +function finishTest() +{ + SimpleTest.executeSoon(function() { + testGenerator.close(); + SimpleTest.finish(); + }); +} + +function grabEventAndContinueHandler(event) +{ + testGenerator.send(event); +} + +function continueToNextStep() +{ + SimpleTest.executeSoon(function() { + testGenerator.next(); + }); +} + +function errorHandler(event) +{ + throw new Error("indexedDB error, code " + event.target.error.name); +} diff --git a/dom/indexedDB/test/error_events_abort_transactions_iframe.html b/dom/indexedDB/test/error_events_abort_transactions_iframe.html new file mode 100644 index 000000000..fd0414975 --- /dev/null +++ b/dom/indexedDB/test/error_events_abort_transactions_iframe.html @@ -0,0 +1,241 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript;version=1.7"> + + let testGenerator = testSteps(); + + function ok(val, message) { + val = val ? "true" : "false"; + window.parent.postMessage("SimpleTest.ok(" + val + ", '" + message + + "');", "*"); + } + + function is(a, b, message) { + ok(a == b, message); + } + + function grabEventAndContinueHandler(event) { + testGenerator.send(event); + } + + function errorHandler(event) { + ok(false, "indexedDB error, code " + event.target.errorCcode); + finishTest(); + } + + function unexpectedSuccessHandler(event) { + ok(false, "got success when it was not expected!"); + finishTest(); + } + + function finishTest() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + testGenerator.close(); + window.parent.postMessage("SimpleTest.finish();", "*"); + }, 0); + }, 0); + } + + window.onerror = function(message, filename, lineno) { + is(message, "ConstraintError", "Expect a constraint error"); + }; + + function testSteps() { + window.parent.SpecialPowers.addPermission("indexedDB", true, document); + + let request = indexedDB.open(window.location.pathname, 1); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = grabEventAndContinueHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onversionchange = function(event) { + event.target.close(); + }; + + is(db.version, 1, "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + + let trans = event.target.transaction; + + trans.oncomplete = unexpectedSuccessHandler; + trans.onabort = grabEventAndContinueHandler; + + let objectStore = db.createObjectStore("foo"); + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + + let originalRequest = request; + request = objectStore.add({}, 1); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + request = objectStore.add({}, 1); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = function(event) { + // Don't do anything! ConstraintError is expected in window.onerror. + } + event = yield undefined; + + is(event.type, "abort", "Got a transaction abort event"); + is(db.version, 0, "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + is(trans.error.name, "ConstraintError", "Right error"); + ok(trans.error === request.error, "Object identity holds"); + is(originalRequest.transaction, trans, "request.transaction should still be set"); + + event = yield undefined; + is(event.type, "error", "Got request error event"); + is(event.target, originalRequest, "error event has right target"); + is(event.target.error.name, "AbortError", "Right error"); + is(originalRequest.transaction, null, "request.transaction should now be null"); + // Skip the verification of ConstraintError in window.onerror. + event.preventDefault(); + + request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + db.onversionchange = function(event) { + event.target.close(); + }; + + event.target.transaction.oncomplete = grabEventAndContinueHandler; + event.target.transaction.onabort = unexpectedSuccessHandler; + + is(db.version, "1", "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + + objectStore = db.createObjectStore("foo"); + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + + objectStore.createIndex("baz", "key.path"); + objectStore.createIndex("dontDeleteMe", ""); + + is(objectStore.indexNames.length, 2, "Correct indexNames length"); + ok(objectStore.indexNames.contains("baz"), "Has correct index"); + ok(objectStore.indexNames.contains("dontDeleteMe"), "Has correct index"); + + let objectStoreForDeletion = db.createObjectStore("bar"); + + is(db.objectStoreNames.length, 2, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("bar"), "Has correct objectStore"); + + objectStoreForDeletion.createIndex("foo", "key.path"); + + is(objectStoreForDeletion.indexNames.length, 1, "Correct indexNames length"); + ok(objectStoreForDeletion.indexNames.contains("foo"), "Has correct index"); + + request = objectStore.add({}, 1); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + request = objectStore.add({}, 1); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = function(event) { + // Expected, but prevent the abort. + event.preventDefault(); + } + event = yield undefined; + + is(event.type, "complete", "Got a transaction complete event"); + + is(db.version, "1", "Correct version"); + is(db.objectStoreNames.length, 2, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + ok(db.objectStoreNames.contains("bar"), "Has correct objectStore"); + + request = indexedDB.open(window.location.pathname, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + db.onversionchange = function(event) { + event.target.close(); + }; + + trans = event.target.transaction; + trans.oncomplete = unexpectedSuccessHandler; + + is(db.version, "2", "Correct version"); + is(db.objectStoreNames.length, 2, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + ok(db.objectStoreNames.contains("bar"), "Has correct objectStore"); + + let createdObjectStore = db.createObjectStore("newlyCreated"); + objectStore = trans.objectStore("foo"); + let deletedObjectStore = trans.objectStore("bar"); + deletedObjectStore.deleteIndex("foo"); + db.deleteObjectStore("bar"); + + createdObjectStore.createIndex("newIndex", "key.path"); + objectStore.createIndex("newIndex", "key.path"); + objectStore.deleteIndex("baz"); + + is(db.objectStoreNames.length, 2, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("newlyCreated"), "Has correct objectStore"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + + is(createdObjectStore.indexNames.length, 1, "Correct indexNames length"); + ok(createdObjectStore.indexNames.contains("newIndex"), "Has correct index"); + + is(objectStore.indexNames.length, 2, "Correct indexNames length"); + ok(objectStore.indexNames.contains("dontDeleteMe"), "Has correct index"); + ok(objectStore.indexNames.contains("newIndex"), "Has correct index"); + + // ConstraintError is expected in window.onerror. + objectStore.add({}, 1); + trans.onabort = grabEventAndContinueHandler; + + event = yield undefined; + + // Test that the world has been restored. + is(db.version, "1", "Correct version"); + is(db.objectStoreNames.length, 2, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + ok(db.objectStoreNames.contains("bar"), "Has correct objectStore"); + + is(objectStore.indexNames.length, 2, "Correct indexNames length"); + ok(objectStore.indexNames.contains("dontDeleteMe"), "Has correct index"); + ok(objectStore.indexNames.contains("baz"), "Has correct index"); + + is(createdObjectStore.indexNames.length, 0, "Correct indexNames length"); + + is(deletedObjectStore.indexNames.length, 1, "Correct indexNames length"); + ok(deletedObjectStore.indexNames.contains("foo"), "Has correct index"); + + request.onerror = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "error", "Got request error event"); + is(event.target.error.name, "AbortError", "Right error"); + // Skip the verification of ConstraintError in window.onerror. + event.preventDefault(); + + finishTest(); + yield undefined; + } + </script> + +</head> + +<body onload="testGenerator.next();"></body> + +</html> diff --git a/dom/indexedDB/test/event_propagation_iframe.html b/dom/indexedDB/test/event_propagation_iframe.html new file mode 100644 index 000000000..c571421c8 --- /dev/null +++ b/dom/indexedDB/test/event_propagation_iframe.html @@ -0,0 +1,148 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript;version=1.7"> + + let testGenerator = testSteps(); + + function ok(val, message) { + val = val ? "true" : "false"; + window.parent.postMessage("SimpleTest.ok(" + val + ", '" + message + + "');", "*"); + } + + function grabEventAndContinueHandler(event) { + testGenerator.send(event); + } + + function errorHandler(event) { + ok(false, "indexedDB error, code " + event.target.error.name); + finishTest(); + } + + function finishTest() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + testGenerator.close(); + ok(windowErrorCount == 1, "Good window.onerror count"); + window.parent.postMessage("SimpleTest.finish();", "*"); + }, 0); + }, 0); + } + + const eventChain = [ + "IDBRequest", + "IDBTransaction", + "IDBDatabase" + ]; + + let captureCount = 0; + let bubbleCount = 0; + let atTargetCount = 0; + let windowErrorCount = 0; + + window.onerror = function(event) { + ok(!windowErrorCount++, "Correct number of window.onerror events"); + setTimeout(function() { testGenerator.next(); }, 0); + }; + + function errorEventCounter(event) { + ok(event.type == "error", "Got an error event"); + ok(event.target instanceof window[eventChain[0]], + "Correct event.target"); + + let constructor; + if (event.eventPhase == event.AT_TARGET) { + atTargetCount++; + constructor = eventChain[0]; + } + else if (event.eventPhase == event.CAPTURING_PHASE) { + constructor = eventChain[eventChain.length - 1 - captureCount++]; + } + else if (event.eventPhase == event.BUBBLING_PHASE) { + constructor = eventChain[++bubbleCount]; + if (windowErrorCount && bubbleCount == eventChain.length - 1) { + event.preventDefault(); + } + } + ok(event.currentTarget instanceof window[constructor], + "Correct event.currentTarget"); + + if (bubbleCount == eventChain.length - 1) { + ok(bubbleCount == captureCount, + "Got same number of calls for both phases"); + ok(atTargetCount == 1, "Got one atTarget event"); + + captureCount = bubbleCount = atTargetCount = 0; + if (windowErrorCount) { + finishTest(); + } + } + } + + function testSteps() { + window.parent.SpecialPowers.addPermission("indexedDB", true, document); + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorEventCounter; + db.addEventListener("error", errorEventCounter, true); + + event.target.onsuccess = grabEventAndContinueHandler; + + db.createObjectStore("foo", { autoIncrement: true }); + yield undefined; + + let transaction = db.transaction("foo", "readwrite"); + transaction.addEventListener("error", errorEventCounter, false); + transaction.addEventListener("error", errorEventCounter, true); + + let objectStore = transaction.objectStore("foo"); + + request = objectStore.add({}, 1); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + request = objectStore.add({}, 1); + request.onsuccess = function(event) { + ok(false, "Did not expect second add to succeed."); + }; + request.onerror = errorEventCounter; + yield undefined; + + transaction = db.transaction("foo", "readwrite"); + transaction.addEventListener("error", errorEventCounter, false); + transaction.addEventListener("error", errorEventCounter, true); + + objectStore = transaction.objectStore("foo"); + + request = objectStore.add({}, 1); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + request = objectStore.add({}, 1); + request.onsuccess = function(event) { + ok(false, "Did not expect second add to succeed."); + }; + request.onerror = errorEventCounter; + yield undefined; + } + </script> + +</head> + +<body onload="testGenerator.next();"></body> + +</html> diff --git a/dom/indexedDB/test/exceptions_in_events_iframe.html b/dom/indexedDB/test/exceptions_in_events_iframe.html new file mode 100644 index 000000000..e8002d56c --- /dev/null +++ b/dom/indexedDB/test/exceptions_in_events_iframe.html @@ -0,0 +1,182 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + let testGenerator = testSteps(); + + function ok(val, message) { + val = val ? "true" : "false"; + window.parent.postMessage("SimpleTest.ok(" + val + ", '" + message + + "');", "*"); + } + + function is(a, b, message) { + ok(a == b, message); + } + + function grabEventAndContinueHandler(event) { + testGenerator.send(event); + } + + function errorHandler(event) { + ok(false, "indexedDB error, code " + event.target.error.name); + finishTest(); + } + + function unexpectedSuccessHandler(event) { + ok(false, "got success when it was not expected!"); + finishTest(); + } + + function finishTest() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + testGenerator.close(); + window.parent.postMessage("SimpleTest.finish();", "*"); + }, 0); + }, 0); + } + + window.onerror = function() { + return false; + }; + + function testSteps() { + window.parent.SpecialPowers.addPermission("indexedDB", true, document); + + // Test 1: Throwing an exception in an upgradeneeded handler should + // abort the versionchange transaction and fire an error at the request. + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onsuccess = unexpectedSuccessHandler; + request.onupgradeneeded = function () { + let transaction = request.transaction; + transaction.oncomplete = unexpectedSuccessHandler; + transaction.onabort = grabEventAndContinueHandler + throw "STOP"; + }; + + let event = yield undefined; + is(event.type, "abort", + "Throwing during an upgradeneeded event should abort the transaction."); + is(event.target.error.name, "AbortError", "Got AbortError object"); + + request.onerror = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "error", + "Throwing during an upgradeneeded event should fire an error."); + + // Test 2: Throwing during a request's success handler should abort the + // transaction. + request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onblocked = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let openrequest = request; + event = yield undefined; + + request.onupgradeneeded = unexpectedSuccessHandler; + + let db = event.target.result; + db.onerror = function(event) { + event.preventDefault(); + }; + + event.target.transaction.oncomplete = unexpectedSuccessHandler; + event.target.transaction.onabort = grabEventAndContinueHandler; + + is(db.version, 1, "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + + let objectStore = db.createObjectStore("foo"); + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + + request = objectStore.add({}, 1); + request.onsuccess = function(event) { + throw "foo"; + }; + + event = yield undefined; + + is(event.type, "abort", "Got transaction abort event"); + is(event.target.error.name, "AbortError", "Got AbortError object"); + openrequest.onerror = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "error", "Got IDBOpenDBRequest error event"); + is(event.target, openrequest, "Right event target"); + is(event.target.error.name, "AbortError", "Right error name"); + + // Test 3: Throwing during a request's error handler should abort the + // transaction, even if preventDefault is called on the error event. + request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onblocked = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + openrequest = request; + event = yield undefined; + + request.onupgradeneeded = unexpectedSuccessHandler; + + db = event.target.result; + db.onerror = function(event) { + event.preventDefault(); + }; + + event.target.transaction.oncomplete = unexpectedSuccessHandler; + event.target.transaction.onabort = grabEventAndContinueHandler; + + is(db.version, 1, "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + + objectStore = db.createObjectStore("foo"); + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames length"); + ok(db.objectStoreNames.contains("foo"), "Has correct objectStore"); + + request = objectStore.add({}, 1); + request.onerror = errorHandler; + request = objectStore.add({}, 1); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = function (event) { + event.preventDefault(); + throw "STOP"; + }; + + event = yield undefined; + + is(event.type, "abort", "Got transaction abort event"); + is(event.target.error.name, "AbortError", "Got AbortError object"); + openrequest.onerror = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "error", "Got IDBOpenDBRequest error event"); + is(event.target, openrequest, "Right event target"); + is(event.target.error.name, "AbortError", "Right error name"); + + finishTest(); + yield undefined; + } + </script> + +</head> + +<body onload="testGenerator.next();"></body> + +</html> diff --git a/dom/indexedDB/test/extensions/bootstrap.js b/dom/indexedDB/test/extensions/bootstrap.js new file mode 100644 index 000000000..357ac462e --- /dev/null +++ b/dom/indexedDB/test/extensions/bootstrap.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function testForExpectedSymbols(stage, data) { + const expectedSymbols = [ "IDBKeyRange", "indexedDB" ]; + for (var symbol of expectedSymbols) { + Services.prefs.setBoolPref("indexeddbtest.bootstrap." + stage + "." + + symbol, symbol in this); + } +} + +function GlobalObjectsComponent() { + this.wrappedJSObject = this; +} + +GlobalObjectsComponent.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]), + + runTest: function() { + const name = "Splendid Test"; + + let ok = this.ok; + let finishTest = this.finishTest; + + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + finishTest(); + } + request.onsuccess = function(event) { + let db = event.target.result; + ok(db, "Got database"); + finishTest(); + } + } +}; + +var gFactory = { + register: function() { + var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + + var classID = Components.ID("{d6f85dcb-537d-447e-b783-75d4b405622d}"); + var description = "IndexedDBTest"; + var contractID = "@mozilla.org/dom/indexeddb/GlobalObjectsComponent;1"; + var factory = XPCOMUtils._getFactory(GlobalObjectsComponent); + + registrar.registerFactory(classID, description, contractID, factory); + + this.unregister = function() { + registrar.unregisterFactory(classID, factory); + delete this.unregister; + }; + } +}; + +function install(data, reason) { + testForExpectedSymbols("install"); +} + +function startup(data, reason) { + testForExpectedSymbols("startup"); + gFactory.register(); +} + +function shutdown(data, reason) { + testForExpectedSymbols("shutdown"); + gFactory.unregister(); +} + +function uninstall(data, reason) { + testForExpectedSymbols("uninstall"); +} diff --git a/dom/indexedDB/test/extensions/indexedDB-test@mozilla.org.xpi b/dom/indexedDB/test/extensions/indexedDB-test@mozilla.org.xpi Binary files differnew file mode 100644 index 000000000..bbe2430e2 --- /dev/null +++ b/dom/indexedDB/test/extensions/indexedDB-test@mozilla.org.xpi diff --git a/dom/indexedDB/test/extensions/install.rdf b/dom/indexedDB/test/extensions/install.rdf new file mode 100644 index 000000000..e7afc68d6 --- /dev/null +++ b/dom/indexedDB/test/extensions/install.rdf @@ -0,0 +1,31 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:name>IndexedDBTest</em:name> + <em:description>IndexedDB functions for use in testing.</em:description> + <em:creator>Mozilla</em:creator> + <em:version>2016.03.09</em:version> + <em:id>indexedDB-test@mozilla.org</em:id> + <em:type>2</em:type> + <em:bootstrap>true</em:bootstrap> + <em:targetApplication> + <Description> + <!-- Firefox --> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>45.0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <Description> + <!-- Fennec --> + <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id> + <em:minVersion>45.0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/dom/indexedDB/test/extensions/moz.build b/dom/indexedDB/test/extensions/moz.build new file mode 100644 index 000000000..a810e8a9e --- /dev/null +++ b/dom/indexedDB/test/extensions/moz.build @@ -0,0 +1,16 @@ +# -*- 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/. + +XPI_NAME = 'indexedDB' + +FINAL_TARGET_FILES += [ + 'bootstrap.js', + 'install.rdf', +] + +TEST_HARNESS_FILES.testing.mochitest.extensions += [ + 'indexedDB-test@mozilla.org.xpi', +] diff --git a/dom/indexedDB/test/file.js b/dom/indexedDB/test/file.js new file mode 100644 index 000000000..a0287cbe0 --- /dev/null +++ b/dom/indexedDB/test/file.js @@ -0,0 +1,266 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var bufferCache = []; +var utils = SpecialPowers.getDOMWindowUtils(window); + +function getBuffer(size) +{ + let buffer = new ArrayBuffer(size); + is(buffer.byteLength, size, "Correct byte length"); + return buffer; +} + +function getRandomBuffer(size) +{ + let buffer = getBuffer(size); + let view = new Uint8Array(buffer); + for (let i = 0; i < size; i++) { + view[i] = parseInt(Math.random() * 255) + } + return buffer; +} + +function getView(size) +{ + let buffer = new ArrayBuffer(size); + let view = new Uint8Array(buffer); + is(buffer.byteLength, size, "Correct byte length"); + return view; +} + +function getRandomView(size) +{ + let view = getView(size); + for (let i = 0; i < size; i++) { + view[i] = parseInt(Math.random() * 255) + } + return view; +} + +function compareBuffers(buffer1, buffer2) +{ + if (buffer1.byteLength != buffer2.byteLength) { + return false; + } + let view1 = buffer1 instanceof Uint8Array ? buffer1 : new Uint8Array(buffer1); + let view2 = buffer2 instanceof Uint8Array ? buffer2 : new Uint8Array(buffer2); + for (let i = 0; i < buffer1.byteLength; i++) { + if (view1[i] != view2[i]) { + return false; + } + } + return true; +} + +function getBlob(type, view) +{ + return new Blob([view], {type: type}); +} + +function getFile(name, type, view) +{ + return new File([view], name, {type: type}); +} + +function getRandomBlob(size) +{ + return getBlob("binary/random", getRandomView(size)); +} + +function getRandomFile(name, size) +{ + return getFile(name, "binary/random", getRandomView(size)); +} + +function getNullBlob(size) +{ + return getBlob("binary/null", getView(size)); +} + +function getNullFile(name, size) +{ + return getFile(name, "binary/null", getView(size)); +} + +// This needs to be async to make it available on workers too. +function getWasmBinary(text) +{ + let binary = getWasmBinarySync(text); + SimpleTest.executeSoon(function() { + testGenerator.send(binary); + }); +} + +function getWasmModule(binary) +{ + let module = new WebAssembly.Module(binary); + return module; +} + +function verifyBuffers(buffer1, buffer2) +{ + ok(compareBuffers(buffer1, buffer2), "Correct buffer data"); +} + +function verifyBlob(blob1, blob2, fileId, blobReadHandler) +{ + is(blob1 instanceof Components.interfaces.nsIDOMBlob, true, + "Instance of nsIDOMBlob"); + is(blob1 instanceof File, blob2 instanceof File, + "Instance of DOM File"); + is(blob1.size, blob2.size, "Correct size"); + is(blob1.type, blob2.type, "Correct type"); + if (blob2 instanceof File) { + is(blob1.name, blob2.name, "Correct name"); + } + is(utils.getFileId(blob1), fileId, "Correct file id"); + + let buffer1; + let buffer2; + + for (let i = 0; i < bufferCache.length; i++) { + if (bufferCache[i].blob == blob2) { + buffer2 = bufferCache[i].buffer; + break; + } + } + + if (!buffer2) { + let reader = new FileReader(); + reader.readAsArrayBuffer(blob2); + reader.onload = function(event) { + buffer2 = event.target.result; + bufferCache.push({ blob: blob2, buffer: buffer2 }); + if (buffer1) { + verifyBuffers(buffer1, buffer2); + if (blobReadHandler) { + blobReadHandler(); + } + else { + testGenerator.next(); + } + } + } + } + + let reader = new FileReader(); + reader.readAsArrayBuffer(blob1); + reader.onload = function(event) { + buffer1 = event.target.result; + if (buffer2) { + verifyBuffers(buffer1, buffer2); + if (blobReadHandler) { + blobReadHandler(); + } + else { + testGenerator.next(); + } + } + } +} + +function verifyBlobArray(blobs1, blobs2, expectedFileIds) +{ + is(blobs1 instanceof Array, true, "Got an array object"); + is(blobs1.length, blobs2.length, "Correct length"); + + if (!blobs1.length) { + return; + } + + let verifiedCount = 0; + + function blobReadHandler() { + if (++verifiedCount == blobs1.length) { + testGenerator.next(); + } + else { + verifyBlob(blobs1[verifiedCount], blobs2[verifiedCount], + expectedFileIds[verifiedCount], blobReadHandler); + } + } + + verifyBlob(blobs1[verifiedCount], blobs2[verifiedCount], + expectedFileIds[verifiedCount], blobReadHandler); +} + +function verifyMutableFile(mutableFile1, file2) +{ + ok(mutableFile1 instanceof IDBMutableFile, "Instance of IDBMutableFile"); + is(mutableFile1.name, file2.name, "Correct name"); + is(mutableFile1.type, file2.type, "Correct type"); + continueToNextStep(); +} + +function verifyView(view1, view2) +{ + is(view1.byteLength, view2.byteLength, "Correct byteLength"); + verifyBuffers(view1, view2); + continueToNextStep(); +} + +function verifyWasmModule(module1, module2) +{ + let getGlobalForObject = SpecialPowers.Cu.getGlobalForObject; + let testingFunctions = SpecialPowers.Cu.getJSTestingFunctions(); + let wasmExtractCode = SpecialPowers.unwrap(testingFunctions.wasmExtractCode); + let exp1 = wasmExtractCode(module1); + let exp2 = wasmExtractCode(module2); + let code1 = exp1.code; + let code2 = exp2.code; + ok(code1 instanceof getGlobalForObject(code1).Uint8Array, "Instance of Uint8Array"); + ok(code2 instanceof getGlobalForObject(code1).Uint8Array, "Instance of Uint8Array"); + ok(code1.length == code2.length, "Correct length"); + verifyBuffers(code1, code2); + continueToNextStep(); +} + +function grabFileUsageAndContinueHandler(request) +{ + testGenerator.send(request.result.fileUsage); +} + +function getCurrentUsage(usageHandler) +{ + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let cb = SpecialPowers.wrapCallback(usageHandler); + qms.getUsageForPrincipal(principal, cb); +} + +function getFileId(file) +{ + return utils.getFileId(file); +} + +function getFilePath(file) +{ + return utils.getFilePath(file); +} + +function hasFileInfo(name, id) +{ + return utils.getFileReferences(name, id); +} + +function getFileRefCount(name, id) +{ + let count = {}; + utils.getFileReferences(name, id, null, count); + return count.value; +} + +function getFileDBRefCount(name, id) +{ + let count = {}; + utils.getFileReferences(name, id, null, {}, count); + return count.value; +} + +function flushPendingFileDeletions() +{ + utils.flushPendingFileDeletions(); +} diff --git a/dom/indexedDB/test/file_app_isolation.html b/dom/indexedDB/test/file_app_isolation.html new file mode 100644 index 000000000..0ff74e689 --- /dev/null +++ b/dom/indexedDB/test/file_app_isolation.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<html> + <body> + foobar! + </body> + <script> + var data = [ + { id: "0", name: "foo" }, + ]; + + var action = window.location.search.substring(1); + var finished = false; + var created = false; // We use that for 'read-no' action. + + function finish(value) { + value ? alert('success') : alert('failure'); + finished = true; + } + + var request = window.indexedDB.open('AppIsolationTest'); + + request.onupgradeneeded = function(event) { + if (finished) { + finish(false); + return; + } + + switch (action) { + case 'read-no': + created = true; + break; + case 'read-yes': + finish(false); + break; + case 'write': + created = true; + + var db = event.target.result; + + var objectStore = db.createObjectStore("test", { keyPath: "id" }); + for (var i in data) { + objectStore.add(data[i]); + } + break; + } + } + + request.onsuccess = function(event) { + if (finished) { + finish(false); + return; + } + + var db = event.target.result; + + // Think about close the db! + switch (action) { + case 'read-no': + db.close(); + + if (created) { // That means we have created it. + indexedDB.deleteDatabase('AppIsolationTest').onsuccess = function() { + finish(true); + }; + } else { + finish(false); + } + break; + case 'read-yes': + db.transaction("test").objectStore("test").get("0").onsuccess = function(event) { + var name = event.target.result.name; + db.close(); + + indexedDB.deleteDatabase('AppIsolationTest').onsuccess = function() { + finish(name == 'foo'); + }; + }; + break; + case 'write': + db.close(); + + // Success only if the db was actually created. + finish(created); + break; + } + }; + </script> +</html> diff --git a/dom/indexedDB/test/file_app_isolation.js b/dom/indexedDB/test/file_app_isolation.js new file mode 100644 index 000000000..d6ed3fab4 --- /dev/null +++ b/dom/indexedDB/test/file_app_isolation.js @@ -0,0 +1,161 @@ +SimpleTest.waitForExplicitFinish(); + +var fileTestOnCurrentOrigin = (location.protocol + '//' + location.host + location.pathname) + .replace('test_', 'file_') + .replace('_inproc', '').replace('_oop', ''); + +var previousPrefs = { + mozBrowserFramesEnabled: undefined, + oop_by_default: undefined, +}; + +try { + previousPrefs.mozBrowserFramesEnabled = SpecialPowers.getBoolPref('dom.mozBrowserFramesEnabled'); +} catch(e) +{ +} + +try { + previousPrefs.oop_by_default = SpecialPowers.getBoolPref('dom.ipc.browser_frames.oop_by_default'); +} catch(e) { +} + +SpecialPowers.setBoolPref('dom.mozBrowserFramesEnabled', true); +SpecialPowers.setBoolPref("dom.ipc.browser_frames.oop_by_default", location.pathname.indexOf('_inproc') == -1); + +SpecialPowers.addPermission("browser", true, window.document); + +var gData = [ + // APP 1 + { + app: 'http://example.org/manifest.webapp', + action: 'read-no', + src: fileTestOnCurrentOrigin, + }, + { + app: 'http://example.org/manifest.webapp', + action: 'write', + src: fileTestOnCurrentOrigin, + }, + { + app: 'http://example.org/manifest.webapp', + action: 'read-yes', + src: fileTestOnCurrentOrigin, + }, + // APP 2 + { + app: 'https://example.com/manifest.webapp', + action: 'read-no', + src: fileTestOnCurrentOrigin, + }, + { + app: 'https://example.com/manifest.webapp', + action: 'write', + src: fileTestOnCurrentOrigin, + }, + { + app: 'https://example.com/manifest.webapp', + action: 'read-yes', + src: fileTestOnCurrentOrigin, + }, + // Browser + { + browser: true, + action: 'read-no', + src: fileTestOnCurrentOrigin, + }, + { + browser: true, + action: 'write', + src: fileTestOnCurrentOrigin, + }, + { + browser: true, + action: 'read-yes', + src: fileTestOnCurrentOrigin, + }, +]; + +function runTest() { + for (var i in gData) { + var iframe = document.createElement('iframe'); + var data = gData[i]; + + if (data.app) { + iframe.setAttribute('mozbrowser', ''); + iframe.setAttribute('mozapp', data.app); + } else if (data.browser) { + iframe.setAttribute('mozbrowser', ''); + } + + if (data.app || data.browser) { + iframe.addEventListener('mozbrowsershowmodalprompt', function(e) { + is(e.detail.message, 'success', 'test number ' + i); + +// document.getElementById('content').removeChild(iframe); + + i++; + if (i >= gData.length) { + if (previousPrefs.mozBrowserFramesEnabled !== undefined) { + SpecialPowers.setBoolPref('dom.mozBrowserFramesEnabled', previousPrefs.mozBrowserFramesEnabled); + } + if (previousPrefs.oop_by_default !== undefined) { + SpecialPowers.setBoolPref("dom.ipc.browser_frames.oop_by_default", previousPrefs.oop_by_default); + } + + SpecialPowers.removePermission("browser", window.document); + + indexedDB.deleteDatabase('AppIsolationTest').onsuccess = function() { + SimpleTest.finish(); + }; + } else { + gTestRunner.next(); + } + }); + } + + iframe.src = data.src + '?' + data.action; + + document.getElementById('content').appendChild(iframe); + + yield undefined; + } +} + +var gTestRunner = runTest(); + +function startTest() { + var request = window.indexedDB.open('AppIsolationTest'); + var created = false; + + request.onupgradeneeded = function(event) { + created = true; + var db = event.target.result; + var data = [ + { id: "0", name: "foo" }, + ]; + var objectStore = db.createObjectStore("test", { keyPath: "id" }); + for (var i in data) { + objectStore.add(data[i]); + } + } + + request.onsuccess = function(event) { + var db = event.target.result; + is(created, true, "we should have created the db"); + + db.transaction("test").objectStore("test").get("0").onsuccess = function(event) { + is(event.target.result.name, 'foo', 'data have been written'); + db.close(); + + gTestRunner.next(); + }; + } +} + +// TODO: remove unsetting network.disable.ipc.security as part of bug 820712 +SpecialPowers.pushPrefEnv({ + "set": [ + ["network.disable.ipc.security", true], + ] +}, startTest); diff --git a/dom/indexedDB/test/head.js b/dom/indexedDB/test/head.js new file mode 100644 index 000000000..898a40e8f --- /dev/null +++ b/dom/indexedDB/test/head.js @@ -0,0 +1,158 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gActiveListeners = {}; + +function registerPopupEventHandler(eventName, callback) { + gActiveListeners[eventName] = function (event) { + if (event.target != PopupNotifications.panel) + return; + PopupNotifications.panel.removeEventListener(eventName, + gActiveListeners[eventName], + false); + delete gActiveListeners[eventName]; + + callback.call(PopupNotifications.panel); + } + PopupNotifications.panel.addEventListener(eventName, + gActiveListeners[eventName], + false); +} + +function unregisterPopupEventHandler(eventName) +{ + PopupNotifications.panel.removeEventListener(eventName, + gActiveListeners[eventName], + false); + delete gActiveListeners[eventName]; +} + +function unregisterAllPopupEventHandlers() +{ + for (let eventName in gActiveListeners) { + PopupNotifications.panel.removeEventListener(eventName, + gActiveListeners[eventName], + false); + } + gActiveListeners = {}; +} + +function triggerMainCommand(popup) +{ + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + // 20, 10 so that the inner button is hit + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); +} + +function triggerSecondaryCommand(popup, index) +{ + info("triggering secondary command, " + index); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + + // Cancel the arrow panel slide-in transition (bug 767133) such that + // it won't interfere with us interacting with the dropdown. + SpecialPowers.wrap(document).getAnonymousNodes(popup)[0].style.transition = "none"; + + notification.button.focus(); + + popup.addEventListener("popupshown", function () { + popup.removeEventListener("popupshown", arguments.callee, false); + + // Press down until the desired command is selected + for (let i = 0; i <= index; i++) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_RETURN", {}); + }, false); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", { altKey: (navigator.platform.indexOf("Mac") == -1) }); +} + +function dismissNotification(popup) +{ + info("dismissing notification"); + executeSoon(function () { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); +} + +function setFinishedCallback(callback, win) +{ + if (!win) { + win = window; + } + ContentTask.spawn(win.gBrowser.selectedBrowser, null, function*() { + return yield new Promise(resolve => { + content.wrappedJSObject.testFinishedCallback = (result, exception) => { + info("got finished callback"); + resolve({result, exception}); + }; + }); + }).then(({result, exception}) => { + callback(result, exception); + }); +} + +function dispatchEvent(eventName) +{ + info("dispatching event: " + eventName); + let event = document.createEvent("Events"); + event.initEvent(eventName, false, false); + gBrowser.selectedBrowser.contentWindow.dispatchEvent(event); +} + +function setPermission(url, permission) +{ + const nsIPermissionManager = Components.interfaces.nsIPermissionManager; + + let uri = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(url, null, null); + let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + let principal = ssm.createCodebasePrincipal(uri, {}); + + Components.classes["@mozilla.org/permissionmanager;1"] + .getService(nsIPermissionManager) + .addFromPrincipal(principal, permission, + nsIPermissionManager.ALLOW_ACTION); +} + +function removePermission(url, permission) +{ + let uri = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(url, null, null); + let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + let principal = ssm.createCodebasePrincipal(uri, {}); + + Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager) + .removeFromPrincipal(principal, permission); +} + +function getPermission(url, permission) +{ + let uri = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(url, null, null); + let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + let principal = ssm.createCodebasePrincipal(uri, {}); + + return Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager) + .testPermissionFromPrincipal(principal, permission); +} diff --git a/dom/indexedDB/test/helpers.js b/dom/indexedDB/test/helpers.js new file mode 100644 index 000000000..e6e27f3f3 --- /dev/null +++ b/dom/indexedDB/test/helpers.js @@ -0,0 +1,628 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); +var archiveReaderEnabled = false; + +// The test js is shared between xpcshell (which has no SpecialPowers object) +// and content mochitests (where the |Components| object is accessible only as +// SpecialPowers.Components). Expose Components if necessary here to make things +// work everywhere. +// +// Even if the real |Components| doesn't exist, we might shim in a simple JS +// placebo for compat. An easy way to differentiate this from the real thing +// is whether the property is read-only or not. +var c = Object.getOwnPropertyDescriptor(this, 'Components'); +if ((!c.value || c.writable) && typeof SpecialPowers === 'object') + Components = SpecialPowers.Components; + +function executeSoon(aFun) +{ + let comp = SpecialPowers.wrap(Components); + + let thread = comp.classes["@mozilla.org/thread-manager;1"] + .getService(comp.interfaces.nsIThreadManager) + .mainThread; + + thread.dispatch({ + run: function() { + aFun(); + } + }, Components.interfaces.nsIThread.DISPATCH_NORMAL); +} + +function clearAllDatabases(callback) { + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).nodePrincipal; + let request = qms.clearStoragesForPrincipal(principal); + let cb = SpecialPowers.wrapCallback(callback); + request.callback = cb; +} + +var testHarnessGenerator = testHarnessSteps(); +testHarnessGenerator.next(); + +function testHarnessSteps() { + function nextTestHarnessStep(val) { + testHarnessGenerator.send(val); + } + + let testScriptPath; + let testScriptFilename; + + let scripts = document.getElementsByTagName("script"); + for (let i = 0; i < scripts.length; i++) { + let src = scripts[i].src; + let match = src.match(/indexedDB\/test\/unit\/(test_[^\/]+\.js)$/); + if (match && match.length == 2) { + testScriptPath = src; + testScriptFilename = match[1]; + break; + } + } + + yield undefined; + + info("Running" + + (testScriptFilename ? " '" + testScriptFilename + "'" : "")); + + info("Pushing preferences"); + + SpecialPowers.pushPrefEnv( + { + "set": [ + ["dom.indexedDB.testing", true], + ["dom.indexedDB.experimental", true], + ["dom.archivereader.enabled", true], + ["dom.workers.latestJSVersion", true], + ["javascript.options.wasm", true] + ] + }, + nextTestHarnessStep + ); + yield undefined; + + info("Pushing permissions"); + + SpecialPowers.pushPermissions( + [ + { + type: "indexedDB", + allow: true, + context: document + } + ], + nextTestHarnessStep + ); + yield undefined; + + info("Clearing old databases"); + + clearAllDatabases(nextTestHarnessStep); + yield undefined; + + if (testScriptFilename && !window.disableWorkerTest) { + info("Running test in a worker"); + + let workerScriptBlob = + new Blob([ "(" + workerScript.toString() + ")();" ], + { type: "text/javascript;version=1.7" }); + let workerScriptURL = URL.createObjectURL(workerScriptBlob); + + let worker = new Worker(workerScriptURL); + + worker._expectingUncaughtException = false; + worker.onerror = function(event) { + if (worker._expectingUncaughtException) { + ok(true, "Worker had an expected error: " + event.message); + worker._expectingUncaughtException = false; + event.preventDefault(); + return; + } + ok(false, "Worker had an error: " + event.message); + worker.terminate(); + nextTestHarnessStep(); + }; + + worker.onmessage = function(event) { + let message = event.data; + switch (message.op) { + case "ok": + ok(message.condition, message.name, message.diag); + break; + + case "todo": + todo(message.condition, message.name, message.diag); + break; + + case "info": + info(message.msg); + break; + + case "ready": + worker.postMessage({ op: "load", files: [ testScriptPath ] }); + break; + + case "loaded": + worker.postMessage({ op: "start", wasmSupported: isWasmSupported() }); + break; + + case "done": + ok(true, "Worker finished"); + nextTestHarnessStep(); + break; + + case "expectUncaughtException": + worker._expectingUncaughtException = message.expecting; + break; + + case "clearAllDatabases": + clearAllDatabases(function(){ + worker.postMessage({ op: "clearAllDatabasesDone" }); + }); + break; + + case "getWasmBinary": + worker.postMessage({ op: "getWasmBinaryDone", + wasmBinary: getWasmBinarySync(message.text) }); + break; + + default: + ok(false, + "Received a bad message from worker: " + JSON.stringify(message)); + nextTestHarnessStep(); + } + }; + + URL.revokeObjectURL(workerScriptURL); + + yield undefined; + + if (worker._expectingUncaughtException) { + ok(false, "expectUncaughtException was called but no uncaught " + + "exception was detected!"); + } + + worker.terminate(); + worker = null; + + clearAllDatabases(nextTestHarnessStep); + yield undefined; + } else if (testScriptFilename) { + todo(false, + "Skipping test in a worker because it is explicitly disabled: " + + disableWorkerTest); + } else { + todo(false, + "Skipping test in a worker because it's not structured properly"); + } + + info("Running test in main thread"); + + // Now run the test script in the main thread. + testGenerator.next(); + + yield undefined; +} + +if (!window.runTest) { + window.runTest = function() + { + SimpleTest.waitForExplicitFinish(); + testHarnessGenerator.next(); + } +} + +function finishTest() +{ + SpecialPowers.notifyObserversInParentProcess(null, + "disk-space-watcher", + "free"); + + SimpleTest.executeSoon(function() { + testGenerator.close(); + testHarnessGenerator.close(); + clearAllDatabases(function() { SimpleTest.finish(); }); + }); +} + +function browserRunTest() +{ + testGenerator.next(); +} + +function browserFinishTest() +{ + setTimeout(function() { testGenerator.close(); }, 0); +} + +function grabEventAndContinueHandler(event) +{ + testGenerator.send(event); +} + +function continueToNextStep() +{ + SimpleTest.executeSoon(function() { + testGenerator.next(); + }); +} + +function continueToNextStepSync() +{ + testGenerator.next(); +} + +function errorHandler(event) +{ + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + finishTest(); +} + +// For error callbacks where the argument is not an event object. +function errorCallbackHandler(err) +{ + ok(false, "got unexpected error callback: " + err); + finishTest(); +} + +function expectUncaughtException(expecting) +{ + SimpleTest.expectUncaughtException(expecting); +} + +function browserErrorHandler(event) +{ + browserFinishTest(); + throw new Error("indexedDB error (" + event.code + "): " + event.message); +} + +function unexpectedSuccessHandler() +{ + ok(false, "Got success, but did not expect it!"); + finishTest(); +} + +function expectedErrorHandler(name) +{ + return function(event) { + is(event.type, "error", "Got an error event"); + is(event.target.error.name, name, "Expected error was thrown."); + event.preventDefault(); + grabEventAndContinueHandler(event); + }; +} + +function ExpectError(name, preventDefault) +{ + this._name = name; + this._preventDefault = preventDefault; +} +ExpectError.prototype = { + handleEvent: function(event) + { + is(event.type, "error", "Got an error event"); + is(event.target.error.name, this._name, "Expected error was thrown."); + if (this._preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } + grabEventAndContinueHandler(event); + } +}; + +function compareKeys(_k1_, _k2_) { + let t = typeof _k1_; + if (t != typeof _k2_) + return false; + + if (t !== "object") + return _k1_ === _k2_; + + if (_k1_ instanceof Date) { + return (_k2_ instanceof Date) && + _k1_.getTime() === _k2_.getTime(); + } + + if (_k1_ instanceof Array) { + if (!(_k2_ instanceof Array) || + _k1_.length != _k2_.length) + return false; + + for (let i = 0; i < _k1_.length; ++i) { + if (!compareKeys(_k1_[i], _k2_[i])) + return false; + } + + return true; + } + + return false; +} + +function removePermission(type, url) +{ + if (!url) { + url = window.document; + } + SpecialPowers.removePermission(type, url); +} + +function gc() +{ + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); +} + +function scheduleGC() +{ + SpecialPowers.exactGC(continueToNextStep); +} + +function isWasmSupported() +{ + let testingFunctions = SpecialPowers.Cu.getJSTestingFunctions(); + return testingFunctions.wasmIsSupported(); +} + +function getWasmBinarySync(text) +{ + let testingFunctions = SpecialPowers.Cu.getJSTestingFunctions(); + let wasmTextToBinary = SpecialPowers.unwrap(testingFunctions.wasmTextToBinary); + let binary = wasmTextToBinary(text); + return binary; +} + +function workerScript() { + "use strict"; + + self.wasmSupported = false; + + self.repr = function(_thing_) { + if (typeof(_thing_) == "undefined") { + return "undefined"; + } + + let str; + + try { + str = _thing_ + ""; + } catch (e) { + return "[" + typeof(_thing_) + "]"; + } + + if (typeof(_thing_) == "function") { + str = str.replace(/^\s+/, ""); + let idx = str.indexOf("{"); + if (idx != -1) { + str = str.substr(0, idx) + "{...}"; + } + } + + return str; + }; + + self.ok = function(_condition_, _name_, _diag_) { + self.postMessage({ op: "ok", + condition: !!_condition_, + name: _name_, + diag: _diag_ }); + }; + + self.is = function(_a_, _b_, _name_) { + let pass = (_a_ == _b_); + let diag = pass ? "" : "got " + repr(_a_) + ", expected " + repr(_b_); + ok(pass, _name_, diag); + }; + + self.isnot = function(_a_, _b_, _name_) { + let pass = (_a_ != _b_); + let diag = pass ? "" : "didn't expect " + repr(_a_) + ", but got it"; + ok(pass, _name_, diag); + }; + + self.todo = function(_condition_, _name_, _diag_) { + self.postMessage({ op: "todo", + condition: !!_condition_, + name: _name_, + diag: _diag_ }); + }; + + self.info = function(_msg_) { + self.postMessage({ op: "info", msg: _msg_ }); + }; + + self.executeSoon = function(_fun_) { + var channel = new MessageChannel(); + channel.port1.postMessage(""); + channel.port2.onmessage = function(event) { _fun_(); }; + }; + + self.finishTest = function() { + if (self._expectingUncaughtException) { + self.ok(false, "expectUncaughtException was called but no uncaught " + + "exception was detected!"); + } + self.postMessage({ op: "done" }); + }; + + self.grabEventAndContinueHandler = function(_event_) { + testGenerator.send(_event_); + }; + + self.continueToNextStep = function() { + executeSoon(function() { + testGenerator.next(); + }); + }; + + self.continueToNextStepSync = function() { + testGenerator.next(); + }; + + self.errorHandler = function(_event_) { + ok(false, "indexedDB error, '" + _event_.target.error.name + "'"); + finishTest(); + }; + + self.unexpectedSuccessHandler = function() + { + ok(false, "Got success, but did not expect it!"); + finishTest(); + }; + + self.expectedErrorHandler = function(_name_) + { + return function(_event_) { + is(_event_.type, "error", "Got an error event"); + is(_event_.target.error.name, _name_, "Expected error was thrown."); + _event_.preventDefault(); + grabEventAndContinueHandler(_event_); + }; + }; + + self.ExpectError = function(_name_, _preventDefault_) + { + this._name = _name_; + this._preventDefault = _preventDefault_; + } + self.ExpectError.prototype = { + handleEvent: function(_event_) + { + is(_event_.type, "error", "Got an error event"); + is(_event_.target.error.name, this._name, "Expected error was thrown."); + if (this._preventDefault) { + _event_.preventDefault(); + _event_.stopPropagation(); + } + grabEventAndContinueHandler(_event_); + } + }; + + self.compareKeys = function(_k1_, _k2_) { + let t = typeof _k1_; + if (t != typeof _k2_) + return false; + + if (t !== "object") + return _k1_ === _k2_; + + if (_k1_ instanceof Date) { + return (_k2_ instanceof Date) && + _k1_.getTime() === _k2_.getTime(); + } + + if (_k1_ instanceof Array) { + if (!(_k2_ instanceof Array) || + _k1_.length != _k2_.length) + return false; + + for (let i = 0; i < _k1_.length; ++i) { + if (!compareKeys(_k1_[i], _k2_[i])) + return false; + } + + return true; + } + + return false; + } + + self.getRandomBuffer = function(_size_) { + let buffer = new ArrayBuffer(_size_); + is(buffer.byteLength, _size_, "Correct byte length"); + let view = new Uint8Array(buffer); + for (let i = 0; i < _size_; i++) { + view[i] = parseInt(Math.random() * 255) + } + return buffer; + }; + + self._expectingUncaughtException = false; + self.expectUncaughtException = function(_expecting_) { + self._expectingUncaughtException = !!_expecting_; + self.postMessage({ op: "expectUncaughtException", expecting: !!_expecting_ }); + }; + + self._clearAllDatabasesCallback = undefined; + self.clearAllDatabases = function(_callback_) { + self._clearAllDatabasesCallback = _callback_; + self.postMessage({ op: "clearAllDatabases" }); + } + + self.onerror = function(_message_, _file_, _line_) { + if (self._expectingUncaughtException) { + self._expectingUncaughtException = false; + ok(true, "Worker: expected exception [" + _file_ + ":" + _line_ + "]: '" + + _message_ + "'"); + return; + } + ok(false, + "Worker: uncaught exception [" + _file_ + ":" + _line_ + "]: '" + + _message_ + "'"); + self.finishTest(); + self.close(); + return true; + }; + + self.isWasmSupported = function() { + return self.wasmSupported; + } + + self.getWasmBinarySync = function(_text_) { + self.ok(false, "This can't be used on workers"); + } + + self.getWasmBinary = function(_text_) { + self.postMessage({ op: "getWasmBinary", text: _text_ }); + } + + self.getWasmModule = function(_binary_) { + let module = new WebAssembly.Module(_binary_); + return module; + } + + self.verifyWasmModule = function(_module) { + self.todo(false, "Need a verifyWasmModule implementation on workers"); + self.continueToNextStep(); + } + + self.onmessage = function(_event_) { + let message = _event_.data; + switch (message.op) { + case "load": + info("Worker: loading " + JSON.stringify(message.files)); + self.importScripts(message.files); + self.postMessage({ op: "loaded" }); + break; + + case "start": + self.wasmSupported = message.wasmSupported; + executeSoon(function() { + info("Worker: starting tests"); + testGenerator.next(); + }); + break; + + case "clearAllDatabasesDone": + info("Worker: all databases are cleared"); + if (self._clearAllDatabasesCallback) { + self._clearAllDatabasesCallback(); + } + break; + + case "getWasmBinaryDone": + info("Worker: get wasm binary done"); + testGenerator.send(message.wasmBinary); + break; + + default: + throw new Error("Received a bad message from parent: " + + JSON.stringify(message)); + } + }; + + self.postMessage({ op: "ready" }); +} diff --git a/dom/indexedDB/test/leaving_page_iframe.html b/dom/indexedDB/test/leaving_page_iframe.html new file mode 100644 index 000000000..690e5ff6e --- /dev/null +++ b/dom/indexedDB/test/leaving_page_iframe.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> +<head> + <script> +var db; +function startDBWork() { + indexedDB.open(parent.location, 1).onupgradeneeded = function(e) { + db = e.target.result; + var trans = e.target.transaction; + if (db.objectStoreNames.contains("mystore")) { + db.deleteObjectStore("mystore"); + } + var store = db.createObjectStore("mystore"); + store.add({ hello: "world" }, 42); + e.target.onsuccess = madeMod; + }; +} + +function madeMod() { + var trans = db.transaction(["mystore"], "readwrite"); + var store = trans. + objectStore("mystore"); + trans.oncomplete = function() { + parent.postMessage("didcommit", "*"); + } + + store.put({ hello: "officer" }, 42).onsuccess = function(e) { + // Make this transaction run until the end of time or until the page is + // navigated away, whichever comes first. + function doGet() { + store.get(42).onsuccess = doGet; + } + doGet(); + document.location = "about:blank"; + } + +} + </script> +</head> +<body onload="startDBWork();"> + This is page one. +</body> +</html> diff --git a/dom/indexedDB/test/mochitest-intl-api.ini b/dom/indexedDB/test/mochitest-intl-api.ini new file mode 100644 index 000000000..8ec4a172c --- /dev/null +++ b/dom/indexedDB/test/mochitest-intl-api.ini @@ -0,0 +1,10 @@ +# 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/. + +[DEFAULT] + +[test_create_locale_aware_index.html] +[test_locale_aware_indexes.html] +[test_locale_aware_index_getAll.html] +[test_locale_aware_index_getAllObjects.html] diff --git a/dom/indexedDB/test/mochitest.ini b/dom/indexedDB/test/mochitest.ini new file mode 100644 index 000000000..4ab55a9dc --- /dev/null +++ b/dom/indexedDB/test/mochitest.ini @@ -0,0 +1,274 @@ +# 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/. + +[DEFAULT] +support-files = + bfcache_iframe1.html + bfcache_iframe2.html + blob_worker_crash_iframe.html + error_events_abort_transactions_iframe.html + event_propagation_iframe.html + exceptions_in_events_iframe.html + file.js + file_app_isolation.html + file_app_isolation.js + helpers.js + leaving_page_iframe.html + service_worker.js + service_worker_client.html + third_party_iframe1.html + third_party_iframe2.html + unit/test_abort_deleted_index.js + unit/test_abort_deleted_objectStore.js + unit/test_add_put.js + unit/test_add_twice_failure.js + unit/test_advance.js + unit/test_autoIncrement.js + unit/test_autoIncrement_indexes.js + unit/test_blob_file_backed.js + unit/test_blocked_order.js + unit/test_clear.js + unit/test_complex_keyPaths.js + unit/test_count.js + unit/test_create_index.js + unit/test_create_index_with_integer_keys.js + unit/test_create_locale_aware_index.js + unit/test_create_objectStore.js + unit/test_cursor_mutation.js + unit/test_cursor_update_updates_indexes.js + unit/test_cursors.js + unit/test_database_onclose.js + unit/test_deleteDatabase.js + unit/test_deleteDatabase_interactions.js + unit/test_deleteDatabase_onblocked.js + unit/test_deleteDatabase_onblocked_duringVersionChange.js + unit/test_event_source.js + unit/test_filehandle_append_read_data.js + unit/test_getAll.js + unit/test_globalObjects_ipc.js + unit/test_globalObjects_other.js + unit/test_globalObjects_xpc.js + unit/test_global_data.js + unit/test_index_empty_keyPath.js + unit/test_index_getAll.js + unit/test_index_getAllObjects.js + unit/test_index_object_cursors.js + unit/test_index_update_delete.js + unit/test_indexes.js + unit/test_indexes_bad_values.js + unit/test_indexes_funny_things.js + unit/test_invalid_cursor.js + unit/test_invalid_version.js + unit/test_invalidate.js + unit/test_key_requirements.js + unit/test_keys.js + unit/test_locale_aware_indexes.js + unit/test_locale_aware_index_getAll.js + unit/test_locale_aware_index_getAllObjects.js + unit/test_lowDiskSpace.js + unit/test_maximal_serialized_object_size.js + unit/test_multientry.js + unit/test_names_sorted.js + unit/test_objectCursors.js + unit/test_objectStore_getAllKeys.js + unit/test_objectStore_inline_autoincrement_key_added_on_put.js + unit/test_objectStore_openKeyCursor.js + unit/test_objectStore_remove_values.js + unit/test_object_identity.js + unit/test_odd_result_order.js + unit/test_open_empty_db.js + unit/test_open_for_principal.js + unit/test_open_objectStore.js + unit/test_optionalArguments.js + unit/test_overlapping_transactions.js + unit/test_persistenceType.js + unit/test_put_get_values.js + unit/test_put_get_values_autoIncrement.js + unit/test_readonly_transactions.js + unit/test_readwriteflush_disabled.js + unit/test_remove_index.js + unit/test_rename_index.js + unit/test_rename_index_errors.js + unit/test_remove_objectStore.js + unit/test_rename_objectStore.js + unit/test_rename_objectStore_errors.js + unit/test_request_readyState.js + unit/test_setVersion.js + unit/test_setVersion_abort.js + unit/test_setVersion_events.js + unit/test_setVersion_exclusion.js + unit/test_setVersion_throw.js + unit/test_storage_manager_estimate.js + unit/test_success_events_after_abort.js + unit/test_table_locks.js + unit/test_table_rollback.js + unit/test_temporary_storage.js + unit/test_traffic_jam.js + unit/test_transaction_abort.js + unit/test_transaction_abort_hang.js + unit/test_transaction_duplicate_store_names.js + unit/test_transaction_error.js + unit/test_transaction_lifetimes.js + unit/test_transaction_lifetimes_nested.js + unit/test_transaction_ordering.js + unit/test_unique_index_update.js + unit/test_view_put_get_values.js + unit/test_wasm_cursors.js + unit/test_wasm_getAll.js + unit/test_wasm_index_getAllObjects.js + unit/test_wasm_indexes.js + unit/test_wasm_put_get_values.js + unit/test_writer_starvation.js + +[test_abort_deleted_index.html] +[test_abort_deleted_objectStore.html] +[test_add_put.html] +[test_add_twice_failure.html] +[test_advance.html] +[test_app_isolation_inproc.html] +# The app isolation tests are only supposed to run in the main process. +skip-if = e10s +[test_app_isolation_oop.html] +# The app isolation tests are only supposed to run in the main process. +skip-if = e10s +[test_autoIncrement.html] +[test_autoIncrement_indexes.html] +[test_bfcache.html] +[test_blob_archive.html] +[test_blob_file_backed.html] +[test_blob_simple.html] +[test_blob_worker_crash.html] +[test_blob_worker_xhr_post.html] +[test_blob_worker_xhr_post_multifile.html] +[test_blob_worker_xhr_read.html] +[test_blob_worker_xhr_read_slice.html] +[test_blocked_order.html] +[test_bug937006.html] +[test_clear.html] +[test_complex_keyPaths.html] +[test_count.html] +[test_create_index.html] +[test_create_index_with_integer_keys.html] +[test_create_objectStore.html] +[test_cursor_mutation.html] +[test_cursor_update_updates_indexes.html] +[test_cursors.html] +[test_database_onclose.html] +[test_deleteDatabase.html] +[test_deleteDatabase_interactions.html] +[test_deleteDatabase_onblocked.html] +[test_deleteDatabase_onblocked_duringVersionChange.html] +[test_error_events_abort_transactions.html] +[test_event_propagation.html] +[test_event_source.html] +[test_exceptions_in_events.html] +[test_file_array.html] +[test_file_cross_database_copying.html] +[test_file_delete.html] +[test_file_os_delete.html] +[test_file_put_deleted.html] +[test_file_put_get_object.html] +[test_file_put_get_values.html] +[test_file_replace.html] +[test_file_resurrection_delete.html] +[test_file_resurrection_transaction_abort.html] +[test_file_sharing.html] +[test_file_transaction_abort.html] +[test_filehandle_append_read_data.html] +[test_filehandle_compat.html] +[test_filehandle_disabled_pref.html] +[test_filehandle_getFile.html] +[test_filehandle_iteration.html] +[test_filehandle_lifetimes.html] +[test_filehandle_lifetimes_nested.html] +[test_filehandle_location.html] +[test_filehandle_ordering.html] +[test_filehandle_overlapping.html] +[test_filehandle_progress_events.html] +[test_filehandle_readonly_exceptions.html] +[test_filehandle_request_readyState.html] +[test_filehandle_serialization.html] +[test_filehandle_store_snapshot.html] +[test_filehandle_stream_tracking.html] +[test_filehandle_success_events_after_abort.html] +[test_filehandle_truncate.html] +[test_filehandle_workers.html] +[test_filehandle_write_read_data.html] +[test_getAll.html] +[test_globalObjects_content.html] +[test_global_data.html] +[test_index_empty_keyPath.html] +[test_index_getAll.html] +[test_index_getAllObjects.html] +[test_index_object_cursors.html] +[test_index_update_delete.html] +[test_indexes.html] +[test_indexes_bad_values.html] +[test_indexes_funny_things.html] +[test_invalid_cursor.html] +[test_invalid_version.html] +[test_invalidate.html] +# disabled for the moment +skip-if = true +[test_key_requirements.html] +[test_keys.html] +[test_leaving_page.html] +[test_lowDiskSpace.html] +[test_maximal_serialized_object_size.html] +[test_message_manager_ipc.html] +# This test is only supposed to run in the main process. +skip-if = e10s +[test_multientry.html] +[test_names_sorted.html] +[test_objectCursors.html] +[test_objectStore_getAllKeys.html] +[test_objectStore_inline_autoincrement_key_added_on_put.html] +[test_objectStore_openKeyCursor.html] +[test_objectStore_remove_values.html] +[test_object_identity.html] +[test_odd_result_order.html] +[test_open_empty_db.html] +[test_open_for_principal.html] +[test_open_objectStore.html] +[test_optionalArguments.html] +[test_overlapping_transactions.html] +[test_persistenceType.html] +[test_put_get_values.html] +[test_put_get_values_autoIncrement.html] +[test_readonly_transactions.html] +[test_readwriteflush_disabled.html] +[test_remove_index.html] +[test_rename_index.html] +[test_rename_index_errors.html] +[test_remove_objectStore.html] +[test_rename_objectStore.html] +[test_rename_objectStore_errors.html] +[test_request_readyState.html] +[test_sandbox.html] +[test_serviceworker.html] +[test_setVersion.html] +[test_setVersion_abort.html] +[test_setVersion_events.html] +[test_setVersion_exclusion.html] +[test_setVersion_throw.html] +[test_storage_manager_estimate.html] +[test_success_events_after_abort.html] +[test_table_locks.html] +[test_table_rollback.html] +[test_third_party.html] +[test_traffic_jam.html] +[test_transaction_abort.html] +[test_transaction_abort_hang.html] +[test_transaction_duplicate_store_names.html] +[test_transaction_error.html] +[test_transaction_lifetimes.html] +[test_transaction_lifetimes_nested.html] +[test_transaction_ordering.html] +[test_unique_index_update.html] +[test_view_put_get_values.html] +[test_wasm_cursors.html] +[test_wasm_getAll.html] +[test_wasm_index_getAllObjects.html] +[test_wasm_indexes.html] +[test_wasm_put_get_values.html] diff --git a/dom/indexedDB/test/service_worker.js b/dom/indexedDB/test/service_worker.js new file mode 100644 index 000000000..eb8fd7f66 --- /dev/null +++ b/dom/indexedDB/test/service_worker.js @@ -0,0 +1,10 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("Error: no clients are currently controlled.\n"); + return; + } + res[0].postMessage(indexedDB ? { available: true } : + { available: false }); + }); +}; diff --git a/dom/indexedDB/test/service_worker_client.html b/dom/indexedDB/test/service_worker_client.html new file mode 100644 index 000000000..c1c98eaab --- /dev/null +++ b/dom/indexedDB/test/service_worker_client.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +<title>controlled page</title> +<script class="testbody" type="text/javascript"> + if (!parent) { + info("service_worker_client.html should not be launched directly!"); + } + + window.onload = function() { + navigator.serviceWorker.onmessage = function(msg) { + // Forward messages coming from the service worker to the test page. + parent.postMessage(msg.data, "*"); + }; + navigator.serviceWorker.ready.then(function(swr) { + parent.postMessage("READY", "*"); + }); + } +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/indexedDB/test/test_abort_deleted_index.html b/dom/indexedDB/test/test_abort_deleted_index.html new file mode 100644 index 000000000..2c5eb6cbc --- /dev/null +++ b/dom/indexedDB/test/test_abort_deleted_index.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Abort Deleted Index Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_abort_deleted_index.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_abort_deleted_objectStore.html b/dom/indexedDB/test/test_abort_deleted_objectStore.html new file mode 100644 index 000000000..a45e52d52 --- /dev/null +++ b/dom/indexedDB/test/test_abort_deleted_objectStore.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Abort Deleted ObjectStore Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_abort_deleted_objectStore.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_add_put.html b/dom/indexedDB/test/test_add_put.html new file mode 100644 index 000000000..9ba08ac55 --- /dev/null +++ b/dom/indexedDB/test/test_add_put.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_add_put.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_add_twice_failure.html b/dom/indexedDB/test/test_add_twice_failure.html new file mode 100644 index 000000000..1b4ef4c6c --- /dev/null +++ b/dom/indexedDB/test/test_add_twice_failure.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_add_twice_failure.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_advance.html b/dom/indexedDB/test/test_advance.html new file mode 100644 index 000000000..04ffc9a1f --- /dev/null +++ b/dom/indexedDB/test/test_advance.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_advance.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_app_isolation_inproc.html b/dom/indexedDB/test/test_app_isolation_inproc.html new file mode 100644 index 000000000..571189877 --- /dev/null +++ b/dom/indexedDB/test/test_app_isolation_inproc.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=756645 +--> +<head> + <title>Test for IndexedDB app isolation (unique process)</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=756645">Mozilla Bug 756645</a> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript;version=1.7" src="file_app_isolation.js"> +</script> +</pre> +</body> +</html> diff --git a/dom/indexedDB/test/test_app_isolation_oop.html b/dom/indexedDB/test/test_app_isolation_oop.html new file mode 100644 index 000000000..571189877 --- /dev/null +++ b/dom/indexedDB/test/test_app_isolation_oop.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=756645 +--> +<head> + <title>Test for IndexedDB app isolation (unique process)</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=756645">Mozilla Bug 756645</a> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript;version=1.7" src="file_app_isolation.js"> +</script> +</pre> +</body> +</html> diff --git a/dom/indexedDB/test/test_autoIncrement.html b/dom/indexedDB/test/test_autoIncrement.html new file mode 100644 index 000000000..23c89f3e7 --- /dev/null +++ b/dom/indexedDB/test/test_autoIncrement.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_autoIncrement.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_autoIncrement_indexes.html b/dom/indexedDB/test/test_autoIncrement_indexes.html new file mode 100644 index 000000000..b162e548c --- /dev/null +++ b/dom/indexedDB/test/test_autoIncrement_indexes.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_autoIncrement_indexes.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_bfcache.html b/dom/indexedDB/test/test_bfcache.html new file mode 100644 index 000000000..5cb737384 --- /dev/null +++ b/dom/indexedDB/test/test_bfcache.html @@ -0,0 +1,67 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript;version=1.7"> + var gOrigMaxTotalViewers = undefined; + function setCachePref(enabled) { + if (enabled) { + is(typeof gOrigMaxTotalViewers, "undefined", + "don't double-enable bfcache"); + SpecialPowers.setBoolPref("browser.sessionhistory.cache_subframes", + true); + gOrigMaxTotalViewers = + SpecialPowers.getIntPref("browser.sessionhistory.max_total_viewers"); + SpecialPowers.setIntPref("browser.sessionhistory.max_total_viewers", + 10); + } + else { + is(typeof gOrigMaxTotalViewers, "number", + "don't double-disable bfcache"); + SpecialPowers.setIntPref("browser.sessionhistory.max_total_viewers", + gOrigMaxTotalViewers); + gOrigMaxTotalViewers = undefined; + try { + SpecialPowers.clearUserPref("browser.sessionhistory.cache_subframes"); + } catch (e) { /* Pref didn't exist, meh */ } + } + } + + function testSteps() + { + var iframe = $("iframe"); + setCachePref(true); + window.onmessage = grabEventAndContinueHandler; + + iframe.src = "bfcache_iframe1.html"; + var event = yield undefined; + is(event.data, "go", "set up database successfully"); + + iframe.src = "bfcache_iframe2.html"; + res = JSON.parse((yield).data); + is(res.version, 2, "version was set correctly"); + is(res.storeCount, 1, "correct set of stores"); + ok(!("blockedFired" in res), "blocked shouldn't fire"); + is(res.value, JSON.stringify({ hello: "world" }), + "correct value found in store"); + + setCachePref(false); + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"> + <iframe id="iframe"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/test_blob_archive.html b/dom/indexedDB/test/test_blob_archive.html new file mode 100644 index 000000000..add1ad7ee --- /dev/null +++ b/dom/indexedDB/test/test_blob_archive.html @@ -0,0 +1,127 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + + function testSteps() + { + const BLOB_DATA = + "504B03040A00000000002E6BF14000000000000000000000000005001C00746573742F" + + "555409000337CA055039CA055075780B000104E803000004E8030000504B0304140000" + + "0008002D6BF1401780E15015000000580200000A001C00746573742F612E7478745554" + + "09000336CA05503ACA055075780B000104E803000004E8030000CB48CDC9C95728CF2F" + + "CA49E1CA18658FB2A9C40600504B03040A00000000002F88EC40662E84701000000010" + + "0000000A001C00746573742F622E74787455540900035A65FF4F42C5055075780B0001" + + "04E803000004E803000068656C6C6F20776F726C642C2032210A504B01021E030A0000" + + "0000002E6BF140000000000000000000000000050018000000000000001000FD410000" + + "0000746573742F555405000337CA055075780B000104E803000004E8030000504B0102" + + "1E031400000008002D6BF1401780E15015000000580200000A00180000000000010000" + + "00B4813F000000746573742F612E747874555405000336CA055075780B000104E80300" + + "0004E8030000504B01021E030A00000000002F88EC40662E847010000000100000000A" + + "0018000000000001000000B48198000000746573742F622E74787455540500035A65FF" + + "4F75780B000104E803000004E8030000504B05060000000003000300EB000000EC0000" + + "000000"; + + const TEST_FILE_1 = "test/a.txt"; + const TEST_FILE_2 = "test/b.txt"; + + let TEST_FILE_1_CONTENTS = ""; + for (let i = 0; i < 50; i++) { + TEST_FILE_1_CONTENTS += "hello world\n"; + } + const TEST_FILE_2_CONTENTS = "hello world, 2!\n"; + + let binaryData = new Uint8Array(BLOB_DATA.length / 2); + for (let i = 0, len = BLOB_DATA.length / 2; i < len; i++) { + let hex = BLOB_DATA[i * 2] + BLOB_DATA[i * 2 + 1]; + binaryData[i] = parseInt(hex, 16); + } + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + let index = objectStore.createIndex("foo", "index"); + + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let data = new Blob([binaryData]); + + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add(data).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let key = event.target.result; + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(key).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let archiveReader = new ArchiveReader(event.target.result); + ok(archiveReader, "Got an ArchiveReader"); + + request = archiveReader.getFilenames(); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + is(event.target.result.length, 2, "Got 2 archive items"); + is(event.target.result[0], TEST_FILE_1, + "First file is '" + TEST_FILE_1 + "'"); + is(event.target.result[1], TEST_FILE_2, + "Second file is '" + TEST_FILE_2 + "'"); + + request = archiveReader.getFile(TEST_FILE_1); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + let fileReader = new FileReader(); + fileReader.readAsText(event.target.result); + fileReader.onload = grabEventAndContinueHandler; + fileReader.onerror = errorHandler; + event = yield undefined; + + // Don't use is() because it prints out 100 lines of text... + ok(event.target.result == TEST_FILE_1_CONTENTS, "Correct text"); + + request = archiveReader.getFile(TEST_FILE_2); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + fileReader = new FileReader(); + fileReader.readAsText(event.target.result); + fileReader.onload = grabEventAndContinueHandler; + fileReader.onerror = errorHandler; + event = yield undefined; + + // Don't use is() because it prints out a newline... + ok(event.target.result == TEST_FILE_2_CONTENTS, "Correct text"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blob_file_backed.html b/dom/indexedDB/test/test_blob_file_backed.html new file mode 100644 index 000000000..3c3f103d6 --- /dev/null +++ b/dom/indexedDB/test/test_blob_file_backed.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>IndexedDB Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_blob_file_backed.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blob_simple.html b/dom/indexedDB/test/test_blob_simple.html new file mode 100644 index 000000000..e7e440719 --- /dev/null +++ b/dom/indexedDB/test/test_blob_simple.html @@ -0,0 +1,281 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + info("Setting up test fixtures: create an IndexedDB database and object store."); + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + let index = objectStore.createIndex("foo", "index"); + + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + + info("Let's create a blob and store it in IndexedDB twice."); + + const BLOB_DATA = ["fun ", "times ", "all ", "around!"]; + const INDEX_KEY = 5; + let blob = new Blob(BLOB_DATA, { type: "text/plain" }); + let data = { blob: blob, index: INDEX_KEY }; + + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add(data).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Added blob to database once"); + + let key = event.target.result; + + objectStore.add(data).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Added blob to database twice"); + + info("Let's retrieve the blob again and verify the contents is the same."); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(key).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Got blob from database"); + + let fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(event.target.result.blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + + info("Let's retrieve it again, create an object URL for the blob, load" + + "it via an XMLHttpRequest, and verify the contents is the same."); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(key).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Got blob from database"); + + let blobURL = URL.createObjectURL(event.target.result.blob); + + let xhr = new XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.onload = grabEventAndContinueHandler; + xhr.send(); + yield undefined; + + URL.revokeObjectURL(blobURL); + + is(xhr.responseText, BLOB_DATA.join(""), "Correct responseText"); + + + info("Retrieve both blob entries from the database and verify contents."); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.mozGetAll().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.length, 2, "Got right number of items"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(event.target.result[0].blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + let cursorResults = []; + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + info("Got item from cursor"); + cursorResults.push(cursor.value); + cursor.continue(); + } + else { + info("Finished cursor"); + continueToNextStep(); + } + }; + yield undefined; + + is(cursorResults.length, 2, "Got right number of items"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(cursorResults[0].blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + + info("Retrieve blobs from database via index and verify contents."); + + index = db.transaction("foo").objectStore("foo").index("foo"); + index.get(INDEX_KEY).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Got blob from database"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(event.target.result.blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + index = db.transaction("foo").objectStore("foo").index("foo"); + index.mozGetAll().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.length, 2, "Got right number of items"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(event.target.result[0].blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + cursorResults = []; + + index = db.transaction("foo").objectStore("foo").index("foo"); + index.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + info("Got item from cursor"); + cursorResults.push(cursor.value); + cursor.continue(); + } + else { + info("Finished cursor"); + continueToNextStep(); + } + }; + yield undefined; + + is(cursorResults.length, 2, "Got right number of items"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(cursorResults[0].blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(cursorResults[1].blob); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + + info("Slice the the retrieved blob and verify its contents."); + + let slice = cursorResults[1].blob.slice(0, BLOB_DATA[0].length); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(slice); + event = yield undefined; + + is(event.target.result, BLOB_DATA[0], "Correct text"); + + + info("Send blob to a worker, read its contents there, and verify results."); + + function workerScript() { + onmessage = function(event) { + var reader = new FileReaderSync(); + postMessage(reader.readAsText(event.data)); + + var slice = event.data.slice(1, 2); + postMessage(reader.readAsText(slice)); + + } + } + + let url = + URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + let worker = new Worker(url); + worker.postMessage(slice); + worker.onmessage = grabEventAndContinueHandler; + event = yield undefined; + + is(event.data, BLOB_DATA[0], "Correct text"); + event = yield undefined; + + is(event.data, BLOB_DATA[0][1], "Correct text"); + + + info("Store a blob back in the database, and keep holding on to the " + + "blob, verifying that it still can be read."); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(key).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let blobFromDB = event.target.result.blob; + info("Got blob from database"); + + let txn = db.transaction("foo", "readwrite"); + txn.objectStore("foo").put(event.target.result, key); + txn.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + info("Stored blob back into database"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(blobFromDB); + event = yield undefined; + + is(event.target.result, BLOB_DATA.join(""), "Correct text"); + + blobURL = URL.createObjectURL(blobFromDB); + + xhr = new XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.onload = grabEventAndContinueHandler; + xhr.send(); + yield undefined; + + URL.revokeObjectURL(blobURL); + + is(xhr.responseText, BLOB_DATA.join(""), "Correct responseText"); + + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blob_worker_crash.html b/dom/indexedDB/test/test_blob_worker_crash.html new file mode 100644 index 000000000..849915b86 --- /dev/null +++ b/dom/indexedDB/test/test_blob_worker_crash.html @@ -0,0 +1,61 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Blob Worker Crash Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + /* + * This tests ensures that if the last live reference to a Blob is on the + * worker and the database has already been shutdown, that there is no crash + * when the owning page gets cleaned up which causes the termination of the + * worker which in turn garbage collects during its shutdown. + * + * We do the IndexedDB stuff in the iframe so we can kill it as part of our + * test. Doing it out here is no good. + */ + + function testSteps() + { + info("Open iframe, wait for it to do its IndexedDB stuff."); + + let iframe = document.getElementById("iframe1"); + window.addEventListener("message", grabEventAndContinueHandler, false); + // Put it in a different origin to be safe + iframe.src = //"http://example.org" + + window.location.pathname.replace( + "test_blob_worker_crash.html", + "blob_worker_crash_iframe.html"); + + let event = yield unexpectedSuccessHandler; + is(event.data.result, "ready", "worker initialized correctly"); + + info("Trigger a GC to clean-up the iframe's main-thread IndexedDB"); + scheduleGC(); + yield undefined; + + info("Kill the iframe, forget about it, trigger a GC."); + iframe.parentNode.removeChild(iframe); + iframe = null; + scheduleGC(); + yield undefined; + + info("If we are still alive, then we win!"); + ok('Did not crash / trigger an assert!'); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + <iframe id="iframe1"></iframe> +</html> diff --git a/dom/indexedDB/test/test_blob_worker_xhr_post.html b/dom/indexedDB/test/test_blob_worker_xhr_post.html new file mode 100644 index 000000000..a421c4256 --- /dev/null +++ b/dom/indexedDB/test/test_blob_worker_xhr_post.html @@ -0,0 +1,113 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const BLOB_DATA = ["fun ", "times ", "all ", "around!"]; + const BLOB_TYPE = "text/plain"; + const BLOB_SIZE = BLOB_DATA.join("").length; + + info("Setting up"); + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + ok(db, "Created database"); + + info("Creating objectStore"); + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "Opened database"); + + let blob = new Blob(BLOB_DATA, { type: BLOB_TYPE }); + + info("Adding blob to database"); + + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add(blob).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let blobKey = event.target.result; + ok(blobKey, "Got a key for the blob"); + + info("Getting blob from the database"); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(blobKey).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + blob = event.target.result; + + ok(blob instanceof Blob, "Got a blob"); + is(blob.size, BLOB_SIZE, "Correct size"); + is(blob.type, BLOB_TYPE, "Correct type"); + + let slice = blob.slice(0, BLOB_DATA[0].length, BLOB_TYPE); + + ok(slice instanceof Blob, "Slice returned a blob"); + is(slice.size, BLOB_DATA[0].length, "Correct size for slice"); + is(slice.type, BLOB_TYPE, "Correct type for slice"); + + info("Sending slice to a worker"); + + function workerScript() { + onmessage = function(event) { + var blob = event.data; + var xhr = new XMLHttpRequest(); + // We just want to make sure the error case doesn't fire; it's fine for + // us to just want a 404. + xhr.open('POST', 'http://mochi.test:8888/does-not-exist', true); + xhr.onload = function() { + postMessage({ status: xhr.status }); + }; + xhr.onerror = function() { + postMessage({ status: 'error' }); + } + xhr.send(blob); + } + } + + let workerScriptUrl = + URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + let xhrWorker = new Worker(workerScriptUrl); + xhrWorker.postMessage(slice); + xhrWorker.onmessage = grabEventAndContinueHandler; + event = yield undefined; + + is(event.data.status, 404, "XHR generated the expected 404"); + xhrWorker.terminate(); + + URL.revokeObjectURL(workerScriptUrl); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blob_worker_xhr_post_multifile.html b/dom/indexedDB/test/test_blob_worker_xhr_post_multifile.html new file mode 100644 index 000000000..c739d2586 --- /dev/null +++ b/dom/indexedDB/test/test_blob_worker_xhr_post_multifile.html @@ -0,0 +1,113 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + /** + * Create a composite/multi-file Blob on the worker, then post it as an XHR + * payload and ensure that we don't hang/generate an assertion/etc. but + * instead generate the expected 404. This test is basically the same as + * test_blob_worker_xhr_post.html except for the composite Blob. + */ + function testSteps() + { + const BLOB_DATA = ["fun ", "times ", "all ", "around!"]; + const BLOB_TYPE = "text/plain"; + const BLOB_SIZE = BLOB_DATA.join("").length; + + info("Setting up"); + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + ok(db, "Created database"); + + info("Creating objectStore"); + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "Opened database"); + + let blob = new Blob(BLOB_DATA, { type: BLOB_TYPE }); + + info("Adding blob to database"); + + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add(blob).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let blobKey = event.target.result; + ok(blobKey, "Got a key for the blob"); + + info("Getting blob from the database"); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(blobKey).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + blob = event.target.result; + + ok(blob instanceof Blob, "Got a blob"); + is(blob.size, BLOB_SIZE, "Correct size"); + is(blob.type, BLOB_TYPE, "Correct type"); + + function workerScript() { + onmessage = function(event) { + var blob = event.data; + var compositeBlob = new Blob(["preceding string. ", blob], + { type: "text/plain" }); + var xhr = new XMLHttpRequest(); + // We just want to make sure the error case doesn't fire; it's fine for + // us to just want a 404. + xhr.open('POST', 'http://mochi.test:8888/does-not-exist', true); + xhr.onload = function() { + postMessage({ status: xhr.status }); + }; + xhr.onerror = function() { + postMessage({ status: 'error' }); + } + xhr.send(compositeBlob); + } + } + + let workerScriptUrl = + URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + let xhrWorker = new Worker(workerScriptUrl); + xhrWorker.postMessage(blob); + xhrWorker.onmessage = grabEventAndContinueHandler; + event = yield undefined; + + is(event.data.status, 404, "XHR generated the expected 404"); + xhrWorker.terminate(); + + URL.revokeObjectURL(workerScriptUrl); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blob_worker_xhr_read.html b/dom/indexedDB/test/test_blob_worker_xhr_read.html new file mode 100644 index 000000000..920dbffae --- /dev/null +++ b/dom/indexedDB/test/test_blob_worker_xhr_read.html @@ -0,0 +1,114 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Blob Read From Worker</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + /** + * Create an IndexedDB-backed Blob, send it to the worker, try and read the + * contents of the Blob from the worker using an XHR. Ideally, we don't + * deadlock the main thread. + */ + function testSteps() + { + const BLOB_DATA = ["Green"]; + const BLOB_TYPE = "text/plain"; + const BLOB_SIZE = BLOB_DATA.join("").length; + + info("Setting up"); + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + ok(db, "Created database"); + + info("Creating objectStore"); + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "Opened database"); + + let blob = new Blob(BLOB_DATA, { type: BLOB_TYPE }); + + info("Adding blob to database"); + + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add(blob).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let blobKey = event.target.result; + ok(blobKey, "Got a key for the blob"); + + info("Getting blob from the database"); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(blobKey).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + blob = event.target.result; + + ok(blob instanceof Blob, "Got a blob"); + is(blob.size, BLOB_SIZE, "Correct size"); + is(blob.type, BLOB_TYPE, "Correct type"); + + info("Sending blob to a worker"); + + function workerScript() { + onmessage = function(event) { + var blob = event.data; + var blobUrl = URL.createObjectURL(blob); + var xhr = new XMLHttpRequest(); + xhr.open('GET', blobUrl, true); + xhr.responseType = 'text'; + xhr.onload = function() { + postMessage({ data: xhr.response }); + URL.revokeObjectURL(blobUrl); + }; + xhr.onerror = function() { + postMessage({ data: null }); + URL.revokeObjectURL(blobUrl); + } + xhr.send(); + } + } + + let workerScriptUrl = + URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + let xhrWorker = new Worker(workerScriptUrl); + xhrWorker.postMessage(blob); + xhrWorker.onmessage = grabEventAndContinueHandler; + event = yield undefined; + + is(event.data.data, "Green", "XHR returned expected payload."); + xhrWorker.terminate(); + + URL.revokeObjectURL(workerScriptUrl); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blob_worker_xhr_read_slice.html b/dom/indexedDB/test/test_blob_worker_xhr_read_slice.html new file mode 100644 index 000000000..564b63f11 --- /dev/null +++ b/dom/indexedDB/test/test_blob_worker_xhr_read_slice.html @@ -0,0 +1,116 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Blob Read From Worker</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + /** + * Create an IndexedDB-backed Blob, send it to the worker, try and read the + * *SLICED* contents of the Blob from the worker using an XHR. This is + * (as of the time of writing this) basically the same as + * test_blob_worker_xhr_read.html but with slicing added. + */ + function testSteps() + { + const BLOB_DATA = ["Green"]; + const BLOB_TYPE = "text/plain"; + const BLOB_SIZE = BLOB_DATA.join("").length; + + info("Setting up"); + + let request = indexedDB.open(window.location.pathname, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + ok(db, "Created database"); + + info("Creating objectStore"); + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "Opened database"); + + let blob = new Blob(BLOB_DATA, { type: BLOB_TYPE }); + + info("Adding blob to database"); + + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + objectStore.add(blob).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let blobKey = event.target.result; + ok(blobKey, "Got a key for the blob"); + + info("Getting blob from the database"); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(blobKey).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + blob = event.target.result; + + ok(blob instanceof Blob, "Got a blob"); + is(blob.size, BLOB_SIZE, "Correct size"); + is(blob.type, BLOB_TYPE, "Correct type"); + + info("Sending blob to a worker"); + + function workerScript() { + onmessage = function(event) { + var blob = event.data; + var slicedBlob = blob.slice(0, 3, "text/plain"); + var blobUrl = URL.createObjectURL(slicedBlob); + var xhr = new XMLHttpRequest(); + xhr.open('GET', blobUrl, true); + xhr.responseType = 'text'; + xhr.onload = function() { + postMessage({ data: xhr.response }); + URL.revokeObjectURL(blobUrl); + }; + xhr.onerror = function() { + postMessage({ data: null }); + URL.revokeObjectURL(blobUrl); + } + xhr.send(); + } + } + + let workerScriptUrl = + URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + let xhrWorker = new Worker(workerScriptUrl); + xhrWorker.postMessage(blob); + xhrWorker.onmessage = grabEventAndContinueHandler; + event = yield undefined; + + is(event.data.data, "Gre", "XHR returned expected sliced payload."); + xhrWorker.terminate(); + + URL.revokeObjectURL(workerScriptUrl); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_blocked_order.html b/dom/indexedDB/test/test_blocked_order.html new file mode 100644 index 000000000..9b82995a3 --- /dev/null +++ b/dom/indexedDB/test/test_blocked_order.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>IndexedDB Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_blocked_order.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_bug847147.html b/dom/indexedDB/test/test_bug847147.html new file mode 100644 index 000000000..9eb0ed3dd --- /dev/null +++ b/dom/indexedDB/test/test_bug847147.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_lifetimes.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +<script> + +var win; +var r1; + +function e() +{ + win = window.open("data:text/html,<body onload='opener.f()'>1", "_blank", ""); +} + +function f() +{ + setTimeout(function() { + r1 = win.document.documentElement; + win.location.replace("data:text/html,<body onload='opener.g()'>2"); + }, 0); +} + +function g() +{ + r1.appendChild(document.createElement("iframe")); + setTimeout(function() { + win.location = "data:text/html,<body onload='opener.h()'>3"; + }, 0); +} + +function h() +{ + win.close(); + ok(true, "This test is looking for assertions so this is irrelevant."); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</head> + +<body onload="e();"> +<button onclick="e();">Start test</button> +</body> +</html> diff --git a/dom/indexedDB/test/test_bug937006.html b/dom/indexedDB/test/test_bug937006.html new file mode 100644 index 000000000..6a95488e2 --- /dev/null +++ b/dom/indexedDB/test/test_bug937006.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> +<head> + <title>Bug 937006 - "Hit MOZ_CRASH(Failed to get caller.)" using setTimeout on IndexedDB call</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<body onload="runTest();"> + <script type="text/javascript;version=1.7"> + + function runTest() { + // doing this IDBRequest should not be able to retrieve the filename and + // line number. + SimpleTest.requestFlakyTimeout("untriaged"); + setTimeout(indexedDB.deleteDatabase.bind(indexedDB), 0, 'x'); + setTimeout(function() { + ok(true, "Still alive"); + SimpleTest.finish(); + }, 10); + } + + </script> +</body> +</html> diff --git a/dom/indexedDB/test/test_clear.html b/dom/indexedDB/test/test_clear.html new file mode 100644 index 000000000..6d1a1f159 --- /dev/null +++ b/dom/indexedDB/test/test_clear.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_clear.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_complex_keyPaths.html b/dom/indexedDB/test/test_complex_keyPaths.html new file mode 100644 index 000000000..83ff438c2 --- /dev/null +++ b/dom/indexedDB/test/test_complex_keyPaths.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_complex_keyPaths.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_count.html b/dom/indexedDB/test/test_count.html new file mode 100644 index 000000000..ebe14b3d5 --- /dev/null +++ b/dom/indexedDB/test/test_count.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_count.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_create_index.html b/dom/indexedDB/test/test_create_index.html new file mode 100644 index 000000000..43b5a6529 --- /dev/null +++ b/dom/indexedDB/test/test_create_index.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_create_index.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_create_index_with_integer_keys.html b/dom/indexedDB/test/test_create_index_with_integer_keys.html new file mode 100644 index 000000000..f032a8b1d --- /dev/null +++ b/dom/indexedDB/test/test_create_index_with_integer_keys.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_create_index_with_integer_keys.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_create_locale_aware_index.html b/dom/indexedDB/test/test_create_locale_aware_index.html new file mode 100644 index 000000000..4053dcd90 --- /dev/null +++ b/dom/indexedDB/test/test_create_locale_aware_index.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_create_locale_aware_index.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_create_objectStore.html b/dom/indexedDB/test/test_create_objectStore.html new file mode 100644 index 000000000..09660a627 --- /dev/null +++ b/dom/indexedDB/test/test_create_objectStore.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_create_objectStore.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_cursor_mutation.html b/dom/indexedDB/test/test_cursor_mutation.html new file mode 100644 index 000000000..fe1ca873c --- /dev/null +++ b/dom/indexedDB/test/test_cursor_mutation.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_cursor_mutation.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_cursor_update_updates_indexes.html b/dom/indexedDB/test/test_cursor_update_updates_indexes.html new file mode 100644 index 000000000..ecc7fc654 --- /dev/null +++ b/dom/indexedDB/test/test_cursor_update_updates_indexes.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_cursor_update_updates_indexes.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_cursors.html b/dom/indexedDB/test/test_cursors.html new file mode 100644 index 000000000..9e803053d --- /dev/null +++ b/dom/indexedDB/test/test_cursors.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_cursors.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_database_onclose.html b/dom/indexedDB/test/test_database_onclose.html new file mode 100644 index 000000000..5a10b64ad --- /dev/null +++ b/dom/indexedDB/test/test_database_onclose.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database DeleteDatabase Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_database_onclose.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_deleteDatabase.html b/dom/indexedDB/test/test_deleteDatabase.html new file mode 100644 index 000000000..a4b8f3eba --- /dev/null +++ b/dom/indexedDB/test/test_deleteDatabase.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database DeleteDatabase Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_deleteDatabase.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_deleteDatabase_interactions.html b/dom/indexedDB/test/test_deleteDatabase_interactions.html new file mode 100644 index 000000000..947e07f39 --- /dev/null +++ b/dom/indexedDB/test/test_deleteDatabase_interactions.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database DeleteDatabase Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_deleteDatabase_interactions.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_deleteDatabase_onblocked.html b/dom/indexedDB/test/test_deleteDatabase_onblocked.html new file mode 100644 index 000000000..817182c30 --- /dev/null +++ b/dom/indexedDB/test/test_deleteDatabase_onblocked.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Onblocked Test During Deleting Database</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_deleteDatabase_onblocked.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_deleteDatabase_onblocked_duringVersionChange.html b/dom/indexedDB/test/test_deleteDatabase_onblocked_duringVersionChange.html new file mode 100644 index 000000000..dd6a00217 --- /dev/null +++ b/dom/indexedDB/test/test_deleteDatabase_onblocked_duringVersionChange.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Onblocked Test During Version Change Transaction</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_deleteDatabase_onblocked_duringVersionChange.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_error_events_abort_transactions.html b/dom/indexedDB/test/test_error_events_abort_transactions.html new file mode 100644 index 000000000..63af5fdd6 --- /dev/null +++ b/dom/indexedDB/test/test_error_events_abort_transactions.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function messageListener(event) { + eval(event.data); + } + + window.addEventListener("message", messageListener, false); + } + </script> + +</head> + +<body onload="runTest();"> + <iframe src="error_events_abort_transactions_iframe.html"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/test_event_propagation.html b/dom/indexedDB/test/test_event_propagation.html new file mode 100644 index 000000000..0456e32d1 --- /dev/null +++ b/dom/indexedDB/test/test_event_propagation.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function messageListener(event) { + eval(event.data); + } + + window.addEventListener("message", messageListener, false); + } + </script> + +</head> + +<body onload="runTest();"> + <iframe src="event_propagation_iframe.html"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/test_event_source.html b/dom/indexedDB/test/test_event_source.html new file mode 100644 index 000000000..6156a964f --- /dev/null +++ b/dom/indexedDB/test/test_event_source.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_event_source.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_exceptions_in_events.html b/dom/indexedDB/test/test_exceptions_in_events.html new file mode 100644 index 000000000..dadf063fa --- /dev/null +++ b/dom/indexedDB/test/test_exceptions_in_events.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function messageListener(event) { + eval(event.data); + } + + window.addEventListener("message", messageListener, false); + } + </script> + +</head> + +<body onload="runTest();"> + <iframe src="exceptions_in_events_iframe.html"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/test_file_array.html b/dom/indexedDB/test/test_file_array.html new file mode 100644 index 000000000..580790d97 --- /dev/null +++ b/dom/indexedDB/test/test_file_array.html @@ -0,0 +1,87 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const b1 = getRandomBlob(10000); + + const b2 = [ getRandomBlob(5000), getRandomBlob(3000), getRandomBlob(12000), + getRandomBlob(17000), getRandomBlob(16000), getRandomBlob(16000), + getRandomBlob(8000) + ]; + + const b3 = [ getRandomBlob(5000), getRandomBlob(3000), getRandomBlob(9000)]; + + const objectStoreData = [ + { key: 1, blobs: [ b1, b1, b1, b1, b1, b1, b1, b1, b1, b1 ], + expectedFileIds: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }, + { key: 2, blobs: [ b2[0], b2[1], b2[2], b2[3], b2[4], b2[5], b2[6] ], + expectedFileIds: [2, 3, 4, 5, 6, 7, 8] }, + { key: 3, blobs: [ b3[0], b3[0], b3[1], b3[2], b3[2], b3[0], b3[0] ], + expectedFileIds: [9, 9, 10, 11, 11, 9, 9] } + ]; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + for (let data of objectStoreData) { + objectStore.add(data.blobs, data.key); + } + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + for (let data of objectStoreData) { + objectStore = db.transaction([objectStoreName]) + .objectStore(objectStoreName); + + request = objectStore.get(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlobArray(event.target.result, data.blobs, data.expectedFileIds); + yield undefined; + } + + is(bufferCache.length, 11, "Correct length"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_cross_database_copying.html b/dom/indexedDB/test/test_file_cross_database_copying.html new file mode 100644 index 000000000..bf65a3a6b --- /dev/null +++ b/dom/indexedDB/test/test_file_cross_database_copying.html @@ -0,0 +1,108 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const databaseInfo = [ + { name: window.location.pathname + "1" }, + { name: window.location.pathname + "2" } + ]; + + const objectStoreName = "Blobs"; + + const fileData = { key: 1, file: getRandomFile("random.bin", 100000) }; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + let databases = []; + for (let info of databaseInfo) { + let request = indexedDB.open(info.name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + objectStore.add(fileData.file, fileData.key); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + databases.push(db); + } + + let refResult; + for (let db of databases) { + let request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + verifyBlob(result, fileData.file, 1); + yield undefined; + + if (!refResult) { + refResult = result; + continue; + } + + isnot(getFilePath(result), getFilePath(refResult), "Different os files"); + } + + for (let i = 1; i < databases.length; i++) { + let db = databases[i]; + + let objectStore = db.transaction([objectStoreName], READ_WRITE) + .objectStore(objectStoreName); + + request = objectStore.add(refResult, 2); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 2, "Got correct key"); + + request = objectStore.get(2); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + verifyBlob(result, refResult, 2); + yield undefined; + + isnot(getFilePath(result), getFilePath(refResult), "Different os files"); + } + + is(bufferCache.length, 2, "Correct length"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_delete.html b/dom/indexedDB/test/test_file_delete.html new file mode 100644 index 000000000..8110c5a8c --- /dev/null +++ b/dom/indexedDB/test/test_file_delete.html @@ -0,0 +1,137 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const fileData1 = { key: 1, file: getRandomFile("random1.bin", 110000) }; + const fileData2 = { key: 2, file: getRandomFile("random2.bin", 120000) }; + const fileData3 = { key: 3, file: getRandomFile("random3.bin", 130000) }; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + objectStore.add(fileData1.file, fileData1.key); + objectStore.add(fileData2.file, fileData2.key); + objectStore.add(fileData3.file, fileData3.key); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let trans = db.transaction([objectStoreName], READ_WRITE); + trans.objectStore(objectStoreName).delete(fileData1.key); + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Got correct event type"); + + is(getFileDBRefCount(name, 1), 0, "Correct db ref count"); + + fileData1.file = null; + fileData2.file = null; + fileData3.file = null; + } + + scheduleGC(); + yield undefined; + + ok(!hasFileInfo(name, 1), "Correct ref count"); + ok(hasFileInfo(name, 2), "Correct ref count"); + ok(hasFileInfo(name, 3), "Correct ref count"); + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.get(fileData2.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + ok(result, "Got result"); + + objectStore.delete(fileData2.key); + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Got correct event type"); + + is(getFileDBRefCount(name, 2), 0, "Correct db ref count"); + + + trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + objectStore.delete(fileData3.key); + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Got correct event type"); + + is(getFileDBRefCount(name, 3), -1, "Correct db ref count"); + + event = null; + result = null; + } + + scheduleGC(); + yield undefined; + + ok(!hasFileInfo(name, 1), "Correct ref count"); + ok(!hasFileInfo(name, 2), "Correct ref count"); + ok(!hasFileInfo(name, 3), "Correct ref count"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_os_delete.html b/dom/indexedDB/test/test_file_os_delete.html new file mode 100644 index 000000000..f4bf7801b --- /dev/null +++ b/dom/indexedDB/test/test_file_os_delete.html @@ -0,0 +1,109 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + getCurrentUsage(grabFileUsageAndContinueHandler); + let startUsage = yield undefined; + + const fileData1 = { + key: 1, + obj: { id: 1, file: getRandomFile("random.bin", 100000) } + }; + const fileData2 = { + key: 2, + obj: { id: 1, file: getRandomFile("random.bin", 100000) } + }; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + objectStore.createIndex("index", "id", { unique: true }); + + objectStore.add(fileData1.obj, fileData1.key); + + request = objectStore.add(fileData2.obj, fileData2.key); + request.addEventListener("error", new ExpectError("ConstraintError", true)); + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + getCurrentUsage(grabFileUsageAndContinueHandler); + let usage = yield undefined; + + is(usage, startUsage + fileData1.obj.file.size + fileData2.obj.file.size, + "Correct file usage"); + + let trans = db.transaction([objectStoreName], READ_WRITE); + trans.objectStore(objectStoreName).delete(fileData1.key); + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Got correct event type"); + + getCurrentUsage(grabFileUsageAndContinueHandler); + usage = yield undefined; + + is(usage, startUsage + fileData1.obj.file.size + fileData2.obj.file.size, + "OS files exists"); + + fileData1.obj.file = null; + fileData2.obj.file = null; + } + + scheduleGC(); + yield undefined; + + // Flush pending file deletions before checking usage. + flushPendingFileDeletions(); + + getCurrentUsage(grabFileUsageAndContinueHandler); + let endUsage = yield undefined; + + is(endUsage, startUsage, "OS files deleted"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_put_deleted.html b/dom/indexedDB/test/test_file_put_deleted.html new file mode 100644 index 000000000..f205a5b03 --- /dev/null +++ b/dom/indexedDB/test/test_file_put_deleted.html @@ -0,0 +1,156 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + /** + * Test that a put of a file-backed Blob/File whose backing file has been + * deleted results in a failure of that put failure. + * + * In order to create a file-backed Blob and ensure that we actually try and + * copy its contents (rather than triggering a reference-count increment), we + * use two separate databases. This test is derived from + * test_file_cross_database_copying.html. + */ + function testSteps() + { + const READ_WRITE = "readwrite"; + + const databaseInfo = [ + { name: window.location.pathname + "1", source: true }, + { name: window.location.pathname + "2", source: false } + ]; + + const objectStoreName = "Blobs"; + + const fileData = { key: 1, file: getRandomFile("random.bin", 10000) }; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + // Open both databases, put the File in the source. + let databases = []; + for (let info of databaseInfo) { + let request = indexedDB.open(info.name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + // We don't expect any errors yet for either database, but will later on. + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + if (info.source) { + objectStore.add(fileData.file, fileData.key); + } + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + databases.push(db); + } + + // Get a reference to the file-backed File. + let fileBackedFile; + for (let db of databases.slice(0, 1)) { + let request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + verifyBlob(result, fileData.file, 1); + yield undefined; + + fileBackedFile = result; + } + + // Delete the backing file... + let fileFullPath = getFilePath(fileBackedFile); + // (We want to chop off the profile root and the resulting path component + // must not start with a directory separator.) + let fileRelPath = + fileFullPath.substring(fileFullPath.search(/[/\\]storage[/\\]default[/\\]/) + 1); + info("trying to delete: " + fileRelPath); + // by using the existing SpecialPowers mechanism to create files and clean + // them up. We clobber our existing content, then trigger deletion to + // clean up after it. + SpecialPowers.createFiles( + [{ name: fileRelPath, data: '' }], + grabEventAndContinueHandler, errorCallbackHandler); + yield undefined; + // This is async without a callback because it's intended for cleanup. + // Since IDB is PBackground, we can't depend on serial ordering, so we need + // to use another async action. + SpecialPowers.removeFiles(); + SpecialPowers.executeAfterFlushingMessageQueue(grabEventAndContinueHandler); + yield undefined; + // The file is now deleted! + + // Try and put the file-backed Blob in the database, expect failure on the + // request and transaction. + info("attempt to store deleted file-backed blob"); // context for NS_WARN_IF + for (let i = 1; i < databases.length; i++) { + let db = databases[i]; + + let trans = db.transaction([objectStoreName], READ_WRITE); + let objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(fileBackedFile, 2); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = expectedErrorHandler("UnknownError"); + trans.onsuccess = unexpectedSuccessHandler; + trans.onerror = expectedErrorHandler("UnknownError"); + // the database will also throw an error. + db.onerror = expectedErrorHandler("UnknownError"); + event = yield undefined; + event = yield undefined; + event = yield undefined; + // the database shouldn't throw any more errors now. + db.onerror = errorHandler; + } + + // Ensure there's nothing with that key in the target database. + info("now that the transaction failed, make sure our put got rolled back"); + for (let i = 1; i < databases.length; i++) { + let db = databases[i]; + + let objectStore = db.transaction([objectStoreName], "readonly") + .objectStore(objectStoreName); + + // Attempt to fetch the key to verify there's nothing in the DB rather + // than the value which could return undefined as a misleading error. + request = objectStore.getKey(2); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + event = yield undefined; + + let result = event.target.result; + is(result, undefined, "no key found"); // (the get returns undefined) + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_put_get_object.html b/dom/indexedDB/test/test_file_put_get_object.html new file mode 100644 index 000000000..7b96f5687 --- /dev/null +++ b/dom/indexedDB/test/test_file_put_get_object.html @@ -0,0 +1,90 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const blob = getRandomBlob(1000); + const file = getRandomFile("random.bin", 100000); + + const objectData1 = { key: 1, object: { foo: blob, bar: blob } }; + const objectData2 = { key: 2, object: { foo: file, bar: file } }; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + objectStore.add(objectData1.object, objectData1.key); + objectStore.add(objectData2.object, objectData2.key); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + objectStore = db.transaction([objectStoreName]) + .objectStore(objectStoreName); + request = objectStore.get(objectData1.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + + verifyBlob(result.foo, blob, 1); + yield undefined; + + verifyBlob(result.bar, blob, 1); + yield undefined; + + objectStore = db.transaction([objectStoreName]) + .objectStore(objectStoreName); + request = objectStore.get(objectData2.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + result = event.target.result; + + verifyBlob(result.foo, file, 2); + yield undefined; + + verifyBlob(result.bar, file, 2); + yield undefined; + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_put_get_values.html b/dom/indexedDB/test/test_file_put_get_values.html new file mode 100644 index 000000000..3f18264d6 --- /dev/null +++ b/dom/indexedDB/test/test_file_put_get_values.html @@ -0,0 +1,104 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const blobData = { key: 1, blob: getRandomBlob(10000) }; + const fileData = { key: 2, file: getRandomFile("random.bin", 100000) }; + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let objectStore = db.transaction([objectStoreName], READ_WRITE) + .objectStore(objectStoreName); + request = objectStore.add(blobData.blob, blobData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, blobData.key, "Got correct key"); + + request = objectStore.get(blobData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlob(event.target.result, blobData.blob, 1); + yield undefined; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(blobData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlob(event.target.result, blobData.blob, 1); + yield undefined; + + objectStore = db.transaction([objectStoreName], READ_WRITE) + .objectStore(objectStoreName); + request = objectStore.add(fileData.file, fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, fileData.key, "Got correct key"); + + request = objectStore.get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlob(event.target.result, fileData.file, 2); + yield undefined; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlob(event.target.result, fileData.file, 2); + yield undefined; + + is(bufferCache.length, 2, "Correct length"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_replace.html b/dom/indexedDB/test/test_file_replace.html new file mode 100644 index 000000000..a7cc5e6d0 --- /dev/null +++ b/dom/indexedDB/test/test_file_replace.html @@ -0,0 +1,70 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const blobData = { key: 42, blobs: [] }; + + for (let i = 0; i < 100; i++) { + blobData.blobs[i] = getRandomBlob(i); + } + + SpecialPowers.pushPrefEnv({ set: [["dom.indexedDB.dataThreshold", -1]] }, + continueToNextStep); + yield undefined; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + for (let i = 0; i < blobData.blobs.length; i++) { + objectStore.put(blobData.blobs[i], blobData.key); + } + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + for (let id = 1; id <= 100; id++) { + let refs = {}; + let dbRefs = {}; + let hasFileInfo = utils.getFileReferences(name, id, null, refs, dbRefs); + ok(hasFileInfo, "Has file info"); + is(refs.value, 1, "Correct ref count"); + is(dbRefs.value, id / 100 >> 0, "Correct db ref count"); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_resurrection_delete.html b/dom/indexedDB/test/test_file_resurrection_delete.html new file mode 100644 index 000000000..e3d852929 --- /dev/null +++ b/dom/indexedDB/test/test_file_resurrection_delete.html @@ -0,0 +1,133 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const fileData = { key: 1, file: getRandomFile("random.bin", 100000) }; + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + objectStore.add(fileData.file, fileData.key); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + objectStore.delete(fileData.key); + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(getFileDBRefCount(name, 1), 0, "Correct db ref count"); + + trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(fileData.file, fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(getFileDBRefCount(name, 1), 1, "Correct db ref count"); + + fileData.file = null; + } + + scheduleGC(); + yield undefined; + + is(getFileRefCount(name, 1), 0, "Correct ref count"); + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + ok(result, "Got result"); + + objectStore.delete(fileData.key); + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(getFileDBRefCount(name, 1), 0, "Correct db ref count"); + + trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(result, fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(getFileDBRefCount(name, 1), 1, "Correct db ref count"); + + event = null; + result = null; + } + + scheduleGC(); + yield undefined; + + is(getFileRefCount(name, 1), 0, "Correct ref count"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_resurrection_transaction_abort.html b/dom/indexedDB/test/test_file_resurrection_transaction_abort.html new file mode 100644 index 000000000..1265833e4 --- /dev/null +++ b/dom/indexedDB/test/test_file_resurrection_transaction_abort.html @@ -0,0 +1,92 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const fileData = { key: 1, file: getRandomFile("random.bin", 100000) }; + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(fileData.file, fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + request = objectStore.get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + ok(result, "Got result"); + + trans.onabort = grabEventAndContinueHandler; + trans.abort(); + event = yield undefined; + + is(getFileDBRefCount(name, 1), 0, "Correct db ref count"); + + trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(result, fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(getFileDBRefCount(name, 1), 1, "Correct db ref count"); + + fileData.file = null; + } + + scheduleGC(); + yield undefined; + + is(getFileRefCount(name, 1), 0, "Correct ref count"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_sharing.html b/dom/indexedDB/test/test_file_sharing.html new file mode 100644 index 000000000..2f03689f3 --- /dev/null +++ b/dom/indexedDB/test/test_file_sharing.html @@ -0,0 +1,103 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreInfo = [ + { name: "Blobs", options: { } }, + { name: "Other Blobs", options: { } } + ]; + + const fileData = { key: 1, file: getRandomFile("random.bin", 100000) }; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + for (let info of objectStoreInfo) { + let objectStore = db.createObjectStore(info.name, info.options); + objectStore.add(fileData.file, fileData.key); + } + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let refResult; + for (let info of objectStoreInfo) { + let objectStore = db.transaction([info.name]) + .objectStore(info.name); + + request = objectStore.get(fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + verifyBlob(result, fileData.file, 1); + yield undefined; + + if (!refResult) { + refResult = result; + continue; + } + + is(getFilePath(result), getFilePath(refResult), "The same os file"); + } + + for (let i = 1; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + + let objectStore = db.transaction([info.name], READ_WRITE) + .objectStore(info.name); + + request = objectStore.add(refResult, 2); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 2, "Got correct key"); + + request = objectStore.get(2); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + verifyBlob(result, refResult, 1); + yield undefined; + + is(getFilePath(result), getFilePath(refResult), "The same os file"); + } + + is(bufferCache.length, 2, "Correct length"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_file_transaction_abort.html b/dom/indexedDB/test/test_file_transaction_abort.html new file mode 100644 index 000000000..8c08c6517 --- /dev/null +++ b/dom/indexedDB/test/test_file_transaction_abort.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const fileData = { key: 1, file: getRandomFile("random.bin", 100000) }; + + { + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(fileData.file, fileData.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, fileData.key, "Got correct key"); + + trans.onabort = grabEventAndContinueHandler; + trans.abort(); + event = yield undefined; + + is(event.type, "abort", "Got correct event type"); + + is(getFileDBRefCount(name, 1), 0, "Correct db ref count"); + + fileData.file = null; + } + + scheduleGC(); + yield undefined; + + ok(!hasFileInfo(name, 1), "Correct ref count"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_append_read_data.html b/dom/indexedDB/test/test_filehandle_append_read_data.html new file mode 100644 index 000000000..984fb915d --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_append_read_data.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_filehandle_append_read_data.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_compat.html b/dom/indexedDB/test/test_filehandle_compat.html new file mode 100644 index 000000000..667fcc99e --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_compat.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.mozCreateFileHandle("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let fileHandle = event.target.result; + fileHandle.onerror = errorHandler; + + let lockedFile = fileHandle.open(); + ok(lockedFile.fileHandle === fileHandle, "Correct property"); + + request = lockedFile.getMetadata({ size: true }); + ok(request.lockedFile === lockedFile, "Correct property"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_disabled_pref.html b/dom/indexedDB/test/test_filehandle_disabled_pref.html new file mode 100644 index 000000000..d695e10ee --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_disabled_pref.html @@ -0,0 +1,204 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"> + </script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const databaseName = window.location.pathname; + const databaseVersion = 1; + const objectStoreName = "foo"; + const mutableFileName = "bar"; + const mutableFileKey = 42; + + info("opening database"); + + let request = indexedDB.open(databaseName, databaseVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + info("creating object store"); + + let db = event.target.result; + db.onerror = errorHandler; + db.onversionchange = function(event) { + is(event.oldVersion, databaseVersion, "got correct oldVersion"); + is(event.newVersion, null, "got correct newVersion"); + db.close(); + }; + + let objectStore = db.createObjectStore(objectStoreName, + { autoIncrement: true }); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("creating mutable file"); + + request = db.createMutableFile(mutableFileName); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + + verifyMutableFile(mutableFile, getFile(mutableFileName, "", "")); + yield undefined; + + objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + info("adding mutable file"); + + request = objectStore.add(mutableFile, mutableFileKey); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("getting mutable file"); + + request = objectStore.get(mutableFileKey); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyMutableFile(event.target.result, + getFile(mutableFileName, "", "")); + yield undefined; + + info("opening database"); + + request = indexedDB.open(databaseName, databaseVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let db2 = event.target.result; + db2.onerror = errorHandler; + db2.onversionchange = function(event) { + is(event.oldVersion, databaseVersion, "got correct oldVersion"); + is(event.newVersion, null, "got correct newVersion"); + db2.close(); + }; + + objectStore = db2.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + info("adding mutable file"); + + request = objectStore.add(mutableFile); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("getting mutable file"); + + request = objectStore.get(event.target.result); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyMutableFile(event.target.result, + getFile(mutableFileName, "", "")); + yield undefined; + + info("setting file handle pref"); + + SpecialPowers.pushPrefEnv({ set: [["dom.fileHandle.enabled", false]] }, + continueToNextStep); + yield undefined; + + info("opening database"); + + request = indexedDB.open(databaseName, databaseVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let db3 = event.target.result; + db3.onerror = errorHandler; + db3.onversionchange = function(event) { + is(event.oldVersion, databaseVersion, "got correct oldVersion"); + is(event.newVersion, null, "got correct newVersion"); + db3.close(); + }; + + info("creating mutable file"); + + try { + db3.createMutableFile(mutableFileName); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + objectStore = db3.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + info("adding mutable file"); + + try { + objectStore.add(mutableFile); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "DataCloneError", "Good error."); + is(e.code, DOMException.DATA_CLONE_ERR, "Good error code."); + } + + info("getting mutable file"); + + request = objectStore.get(mutableFileKey); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + try { + let result = request.result; + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + info("deleting database"); + + request = indexedDB.deleteDatabase(databaseName); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + info("resetting file handle pref"); + + SpecialPowers.popPrefEnv(continueToNextStep); + yield undefined; + + finishTest(); + yield undefined; + } + </script> + + <script type="text/javascript;version=1.7" src="helpers.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + + </head> + + <body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_getFile.html b/dom/indexedDB/test/test_filehandle_getFile.html new file mode 100644 index 000000000..f17689f1d --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_getFile.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + request = mutableFile.getFile(); + ok(request instanceof DOMRequest, "Correct interface"); + ok(!(request instanceof IDBFileRequest), "Correct interface"); + ok(!('fileHandle' in request), "Property should not exist"); + ok(request.fileHandle === undefined, "Property should not exist"); + ok(!('lockedFile' in request), "Property should not exist"); + ok(request.lockedFile === undefined, "Property should not exist"); + ok(!('onprogress' in request), "Property should not exist"); + ok(request.onprogress === undefined, "Property should not exist"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_iteration.html b/dom/indexedDB/test/test_filehandle_iteration.html new file mode 100644 index 000000000..ebbc8b8be --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_iteration.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const dbName = window.location.pathname; + const dbVersion = 1; + const objectStoreName = "foo"; + const entryCount = 10; + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + db.createObjectStore(objectStoreName, { autoIncrement: true }); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + request = db.createMutableFile("bar"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + + let trans = db.transaction(objectStoreName, "readwrite"); + let objectStore = trans.objectStore(objectStoreName); + + for (let i = 0; i < entryCount; i++) { + request = objectStore.add(mutableFile); + } + + let seenEntryCount = 0; + + request = objectStore.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + seenEntryCount++; + cursor.continue(); + } else { + continueToNextStep(); + } + } + yield undefined; + + is(seenEntryCount, entryCount, "Correct entry count"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_lifetimes.html b/dom/indexedDB/test/test_filehandle_lifetimes.html new file mode 100644 index 000000000..6e4946821 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_lifetimes.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open(); + continueToNextStep(); + yield undefined; + + try { + fileHandle.getMetadata({ size: true }); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "FileHandleInactiveError", "Good error."); + is(e.code, 0, "Good error code."); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_lifetimes_nested.html b/dom/indexedDB/test/test_filehandle_lifetimes_nested.html new file mode 100644 index 000000000..78fc235e8 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_lifetimes_nested.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open(); + + let fileHandle2; + + let comp = SpecialPowers.wrap(SpecialPowers.Components); + let thread = comp.classes["@mozilla.org/thread-manager;1"] + .getService(comp.interfaces.nsIThreadManager) + .currentThread; + + let eventHasRun; + + thread.dispatch(function() { + eventHasRun = true; + + fileHandle2 = mutableFile.open(); + }, SpecialPowers.Ci.nsIThread.DISPATCH_NORMAL); + + while (!eventHasRun) { + thread.processNextEvent(false); + } + + ok(fileHandle2, "Non-null fileHandle2"); + + continueToNextStep(); + yield undefined; + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_location.html b/dom/indexedDB/test/test_filehandle_location.html new file mode 100644 index 000000000..332fc9af1 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_location.html @@ -0,0 +1,104 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + is(fileHandle.location, 0, "Correct location"); + + fileHandle.location = 100000; + is(fileHandle.location, 100000, "Correct location"); + + fileHandle.location = null; + ok(fileHandle.location === null, "Correct location"); + + try { + fileHandle.readAsArrayBuffer(1); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + try { + fileHandle.readAsText(1); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + try { + fileHandle.write({}); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + request = fileHandle.append("foo"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(fileHandle.location === null, "Correct location"); + + try { + fileHandle.truncate(); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + request = fileHandle.truncate(0); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(fileHandle.location, 0, "Correct location"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_ordering.html b/dom/indexedDB/test/test_filehandle_ordering.html new file mode 100644 index 000000000..0f402ed07 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_ordering.html @@ -0,0 +1,62 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle1 = mutableFile.open("readwrite"); + let fileHandle2 = mutableFile.open("readwrite"); + + let request1 = fileHandle2.write("2"); + let request2 = fileHandle1.write("1"); + + fileHandle1.oncomplete = grabEventAndContinueHandler; + fileHandle2.oncomplete = grabEventAndContinueHandler; + + yield undefined; + yield undefined; + + let fileHandle3 = mutableFile.open("readonly"); + let request3 = fileHandle3.readAsText(1); + request3.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.target.result, "2", "File handles were ordered properly."); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_overlapping.html b/dom/indexedDB/test/test_filehandle_overlapping.html new file mode 100644 index 000000000..9f4cf8e6d --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_overlapping.html @@ -0,0 +1,73 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + for (let i = 0; i < 50; i++) { + let stepNumber = 0; + + request = mutableFile.open("readwrite").append("string1"); + request.onsuccess = function(event) { + is(stepNumber, 1, "This callback came first"); + stepNumber++; + event.target.fileHandle.oncomplete = grabEventAndContinueHandler; + } + + request = mutableFile.open("readwrite").append("string2"); + request.onsuccess = function(event) { + is(stepNumber, 2, "This callback came second"); + stepNumber++; + event.target.fileHandle.oncomplete = grabEventAndContinueHandler; + } + + request = mutableFile.open("readwrite").append("string3"); + request.onsuccess = function(event) { + is(stepNumber, 3, "This callback came third"); + stepNumber++; + event.target.fileHandle.oncomplete = grabEventAndContinueHandler; + } + + stepNumber++; + yield undefined; yield undefined; yield undefined;; + + is(stepNumber, 4, "All callbacks received"); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_progress_events.html b/dom/indexedDB/test/test_filehandle_progress_events.html new file mode 100644 index 000000000..0d2c7f7cb --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_progress_events.html @@ -0,0 +1,79 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + var testBuffer = getRandomBuffer(100000); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + + let sum = 0; + + request = fileHandle.write(testBuffer); + request.onprogress = function(event) { + let loaded = event.loaded; + let total = event.total; + ok(loaded >= 0 && loaded <= total, "Correct loaded progress"); + is(total, testBuffer.byteLength, "Correct total progress"); + sum += event.loaded - sum; + } + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(sum, testBuffer.byteLength, "Correct loaded progress sum"); + + sum = 0; + + fileHandle.location = 0; + request = fileHandle.readAsArrayBuffer(testBuffer.byteLength); + request.onprogress = function(event) { + let loaded = event.loaded; + let total = event.total; + ok(loaded >= 0 && loaded <= total, "Correct loaded progress"); + is(total, testBuffer.byteLength, "Correct total progress"); + sum += event.loaded - sum; + } + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(sum, testBuffer.byteLength, "Correct loaded progress sum"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_readonly_exceptions.html b/dom/indexedDB/test/test_filehandle_readonly_exceptions.html new file mode 100644 index 000000000..566e5361b --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_readonly_exceptions.html @@ -0,0 +1,81 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + request = mutableFile.open("readwrite").write({}); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.fileHandle.mode, "readwrite", "Correct mode"); + + try { + mutableFile.open().write({}); + ok(false, "Writing to a readonly file handle should fail!"); + } + catch (e) { + ok(true, "Writing to a readonly file handle failed"); + } + + try { + mutableFile.open().append({}); + ok(false, "Appending to a readonly file handle should fail!"); + } + catch (e) { + ok(true, "Appending to a readonly file handle failed"); + } + + try { + mutableFile.open().truncate({}); + ok(false, "Truncating a readonly file handle should fail!"); + } + catch (e) { + ok(true, "Truncating a readonly file handle failed"); + } + + try { + mutableFile.open().flush({}); + ok(false, "Flushing a readonly file handle should fail!"); + } + catch (e) { + ok(true, "Flushing a readonly file handle failed"); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_request_readyState.html b/dom/indexedDB/test/test_filehandle_request_readyState.html new file mode 100644 index 000000000..d9cea030c --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_request_readyState.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + is(request.readyState, "pending", "Correct readyState"); + + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(request.readyState, "done", "Correct readyState"); + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + is(request.readyState, "pending", "Correct readyState"); + + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(request.readyState, "done", "Correct readyState"); + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + request = fileHandle.write("string"); + is(request.readyState, "pending", "Correct readyState"); + + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(request.readyState, "done", "Correct readyState"); + + fileHandle.location = 0; + request = fileHandle.readAsText(6); + request.onsuccess = grabEventAndContinueHandler; + is(request.readyState, "pending", "Correct readyState"); + event = yield undefined; + + ok(event.target.result, "Got something"); + is(request.readyState, "done", "Correct readyState"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_serialization.html b/dom/indexedDB/test/test_filehandle_serialization.html new file mode 100644 index 000000000..703a40e75 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_serialization.html @@ -0,0 +1,101 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const databaseInfo = [ + { name: window.location.pathname + "1" }, + { name: window.location.pathname + "2" } + ]; + + const objectStoreName = "Blobs"; + + const testFile = getRandomFile("random.bin", 100000); + + let databases = []; + for (let info of databaseInfo) { + let request = indexedDB.open(info.name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + databases.push(db); + } + + let db1 = databases[0]; + + let request = db1.createMutableFile("random.bin", "binary/random"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + is(getFileId(mutableFile), 1, "Correct file id"); + is(mutableFile.name, "random.bin", "Correct name"); + is(mutableFile.type, "binary/random", "Correct type"); + + let trans = db1.transaction([objectStoreName], READ_WRITE); + let objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(mutableFile, 42); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + request = objectStore.get(42); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + is(getFileId(result), 1, "Correct file id"); + is(result.name, mutableFile.name, "Correct name"); + is(result.type, mutableFile.type, "Correct type"); + + let db2 = databases[1]; + + trans = db2.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + try { + objectStore.add(mutableFile, 42); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "DataCloneError", "Good error."); + is(e.code, DOMException.DATA_CLONE_ERR, "Good error code."); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_store_snapshot.html b/dom/indexedDB/test/test_filehandle_store_snapshot.html new file mode 100644 index 000000000..82d14256f --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_store_snapshot.html @@ -0,0 +1,98 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const READ_WRITE = "readwrite"; + + const name = window.location.pathname; + + const objectStoreName = "Blobs"; + + const testFile = getRandomFile("random.bin", 100000); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + request = db.createMutableFile("random.bin", "binary/random"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + + is(getFileId(mutableFile), 1, "Correct file id"); + + request = fileHandle.write(testFile); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + request = mutableFile.getFile(); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let file = event.target.result; + + let trans = db.transaction([objectStoreName], READ_WRITE); + objectStore = trans.objectStore(objectStoreName); + + request = objectStore.add(file, 42); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + // At this moment, the file should not be readable anymore. + let reader = new FileReader(); + try { + reader.readAsArrayBuffer(file); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "FileHandleInactiveError", "Good error."); + is(e.code, 0, "Good error code."); + } + + request = objectStore.get(42); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlob(event.target.result, testFile, 2); + yield undefined; + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_stream_tracking.html b/dom/indexedDB/test/test_filehandle_stream_tracking.html new file mode 100644 index 000000000..f85ff6fc3 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_stream_tracking.html @@ -0,0 +1,112 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + var testBuffer = getRandomBuffer(100000); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + + request = fileHandle.write(testBuffer); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + request = mutableFile.getFile(); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let file = event.target.result; + + let resultBuffer1; + let resultBuffer2; + + let reader1 = new FileReader(); + reader1.readAsArrayBuffer(file); + reader1.onerror = errorHandler; + reader1.onload = function(event) + { + resultBuffer1 = event.target.result; + + let reader = new FileReader(); + try { + reader.readAsArrayBuffer(file); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "FileHandleInactiveError", "Good error."); + is(e.code, 0, "Good error code."); + } + + if (resultBuffer2) { + testGenerator.next(); + } + } + + let reader2 = new FileReader(); + reader2.readAsArrayBuffer(file); + reader2.onerror = errorHandler; + reader2.onload = function(event) + { + resultBuffer2 = event.target.result; + + let reader = new FileReader(); + try { + reader.readAsArrayBuffer(file); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "FileHandleInactiveError", "Good error."); + is(e.code, 0, "Good error code."); + } + + if (resultBuffer1) { + testGenerator.next(); + } + } + + yield undefined; + + ok(compareBuffers(resultBuffer1, testBuffer), "Correct data"); + ok(compareBuffers(resultBuffer2, testBuffer), "Correct data"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_success_events_after_abort.html b/dom/indexedDB/test/test_filehandle_success_events_after_abort.html new file mode 100644 index 000000000..5b0c4de4e --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_success_events_after_abort.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open(); + + fileHandle.oncomplete = unexpectedSuccessHandler; + fileHandle.onabort = grabEventAndContinueHandler; + + let sawError = false; + + request = fileHandle.getMetadata({ size: true }); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = function(event) { + is(event.target.error.name, "AbortError", "Good error"); + sawError = true; + event.stopPropagation(); + } + + fileHandle.abort(); + + event = yield undefined; + + is(event.type, "abort", "Got abort event"); + is(sawError, true, "Saw getMetadata() error"); + + // Make sure the success event isn't queued somehow. + let comp = SpecialPowers.wrap(SpecialPowers.Components); + var thread = comp.classes["@mozilla.org/thread-manager;1"] + .getService(comp.interfaces.nsIThreadManager) + .currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(false); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_truncate.html b/dom/indexedDB/test/test_filehandle_truncate.html new file mode 100644 index 000000000..4dfe331f3 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_truncate.html @@ -0,0 +1,91 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + var testBuffer = getRandomBuffer(100000); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.bin"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + request = fileHandle.write(testBuffer); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(fileHandle.location, 100000, "Correct location"); + + for (let i = 0; i < 10; i++) { + let location = fileHandle.location - 10000; + fileHandle.location = location; + + request = fileHandle.truncate(); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(fileHandle.location, location, "Correct location"); + + request = fileHandle.getMetadata({ size: true }); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.size, location, "Correct size"); + } + + request = fileHandle.write(testBuffer); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let location = fileHandle.location; + for (let i = 0; i < 10; i++) { + location -= 10000; + + request = fileHandle.truncate(location); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(fileHandle.location, location, "Correct location"); + + request = fileHandle.getMetadata({ size: true }); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.size, location, "Correct size"); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_workers.html b/dom/indexedDB/test/test_filehandle_workers.html new file mode 100644 index 000000000..632ab24fd --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_workers.html @@ -0,0 +1,151 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + let testBuffer = getRandomBuffer(100000); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = errorHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + db.createObjectStore("Foo", { }); + + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + + function dummyWorkerScript() { + onmessage = function(event) { + throw("Shouldn't be called!"); + } + } + + let url = + URL.createObjectURL(new Blob(["(", dummyWorkerScript.toSource(), ")()"])); + + let worker1 = new Worker(url); + try { + worker1.postMessage(mutableFile); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "DataCloneError", "Good error."); + is(e.code, DOMException.DATA_CLONE_ERR, "Good error code.") + } + + mutableFile.onerror = errorHandler; + + let fileHandle = mutableFile.open("readwrite"); + + request = fileHandle.write(testBuffer); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + request = mutableFile.getFile(); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let file = event.target.result; + + let worker2 = new Worker(url); + URL.revokeObjectURL(url); + try { + worker2.postMessage(file); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got exception."); + is(e.name, "DataCloneError", "Good error."); + is(e.code, DOMException.DATA_CLONE_ERR, "Good error code.") + } + + let objectStore = + db.transaction("Foo", "readwrite").objectStore("Foo"); + + request = objectStore.add(mutableFile, 42); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + function workerScript() { + onmessage = function(event) { + var name = event.data; + var request = indexedDB.open(name, 1); + request.onsuccess = function(event) { + var db = event.target.result; + let objectStore = db.transaction("Foo").objectStore("Foo"); + request = objectStore.get(42); + request.onsuccess = function(event) { + try { + let result = request.result; + postMessage("error"); + } + catch (e) { + postMessage("success"); + } + } + request.onerror = function(event) { + postMessage("error"); + } + } + request.onerror = function(event) { + postMessage("error"); + } + } + } + + url = URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"])); + + let worker3 = new Worker(url); + URL.revokeObjectURL(url); + worker3.postMessage(name); + worker3.onmessage = grabEventAndContinueHandler; + event = yield undefined; + + is(event.data, "success", "Good response."); + + todo(false, "Terminate all workers at the end of the test to work around bug 1340941."); + worker1.terminate(); + worker2.terminate(); + worker3.terminate(); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_filehandle_write_read_data.html b/dom/indexedDB/test/test_filehandle_write_read_data.html new file mode 100644 index 000000000..036d57117 --- /dev/null +++ b/dom/indexedDB/test/test_filehandle_write_read_data.html @@ -0,0 +1,110 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + var testString = "Lorem ipsum his ponderum delicatissimi ne, at noster dolores urbanitas pro, cibo elaboraret no his. Ea dicunt maiorum usu. Ad appareat facilisis mediocritatem eos. Tale graeci mentitum in eos, hinc insolens at nam. Graecis nominavi aliquyam eu vix. Id solet assentior sadipscing pro. Et per atqui graecis, usu quot viris repudiandae ei, mollis evertitur an nam. At nam dolor ignota, liber labore omnesque ea mei, has movet voluptaria in. Vel an impetus omittantur. Vim movet option salutandi ex, ne mei ignota corrumpit. Mucius comprehensam id per. Est ea putant maiestatis."; + for (let i = 0; i < 5; i++) { + testString += testString; + } + + var testBuffer = getRandomBuffer(100000); + + var testBlob = new Blob([testBuffer], {type: "binary/random"}); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let location = 0; + + let fileHandle = mutableFile.open("readwrite"); + is(fileHandle.location, location, "Correct location"); + + request = fileHandle.write(testString); + location += testString.length; + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + fileHandle.location = 0; + request = fileHandle.readAsText(testString.length); + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let resultString = event.target.result; + ok(resultString == testString, "Correct string data"); + + request = fileHandle.write(testBuffer); + location += testBuffer.byteLength; + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + fileHandle.location -= testBuffer.byteLength; + request = fileHandle.readAsArrayBuffer(testBuffer.byteLength); + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let resultBuffer = event.target.result; + ok(compareBuffers(resultBuffer, testBuffer), "Correct array buffer data"); + + request = fileHandle.write(testBlob); + location += testBlob.size; + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + fileHandle.location -= testBlob.size; + request = fileHandle.readAsArrayBuffer(testBlob.size); + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + resultBuffer = event.target.result; + ok(compareBuffers(resultBuffer, testBuffer), "Correct blob data"); + + request = fileHandle.getMetadata({ size: true }); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + is(result.size, location, "Correct size"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_getAll.html b/dom/indexedDB/test/test_getAll.html new file mode 100644 index 000000000..770d6fcb7 --- /dev/null +++ b/dom/indexedDB/test/test_getAll.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_getAll.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_getFileId.html b/dom/indexedDB/test/test_getFileId.html new file mode 100644 index 000000000..c36b9bc1f --- /dev/null +++ b/dom/indexedDB/test/test_getFileId.html @@ -0,0 +1,32 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>File Handle Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + let id = getFileId(null); + ok(id == -1, "Correct id"); + + id = getFileId(getRandomBlob(100)); + ok(id == -1, "Correct id"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_globalObjects_chrome.xul b/dom/indexedDB/test/test_globalObjects_chrome.xul new file mode 100644 index 000000000..47e967d96 --- /dev/null +++ b/dom/indexedDB/test/test_globalObjects_chrome.xul @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Mozilla Bug 832883" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript;version=1.7"> + <![CDATA[ + function testSteps() { + const name = window.location.pathname; + + // Test for IDBKeyRange and indexedDB availability in chrome windows. + var keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + var request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + ok(db, "Got database"); + + finishTest(); + yield undefined; + } + ]]> + </script> + + <script type="text/javascript;version=1.7" src="chromeHelpers.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=832883" + target="_blank">Mozilla Bug 832883</a> + </body> +</window> diff --git a/dom/indexedDB/test/test_globalObjects_content.html b/dom/indexedDB/test/test_globalObjects_content.html new file mode 100644 index 000000000..8a91a9ca3 --- /dev/null +++ b/dom/indexedDB/test/test_globalObjects_content.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + + // Test for IDBKeyRange and indexedDB availability in content windows. + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + ok(db, "Got database"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_globalObjects_other.xul b/dom/indexedDB/test/test_globalObjects_other.xul new file mode 100644 index 000000000..eb180a9b4 --- /dev/null +++ b/dom/indexedDB/test/test_globalObjects_other.xul @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Mozilla Bug 832883" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript;version=1.7"> + <![CDATA[ + function testSteps() { + // Test for IDBKeyRange and indexedDB availability in bootstrap files. + let test = Cc["@mozilla.org/dom/indexeddb/GlobalObjectsComponent;1"]. + createInstance(Ci.nsISupports).wrappedJSObject; + test.ok = ok; + test.finishTest = continueToNextStep; + test.runTest(); + yield undefined; + + Cu.import("resource://gre/modules/AddonManager.jsm"); + AddonManager.getAddonByID("indexedDB-test@mozilla.org", + grabEventAndContinueHandler); + let addon = yield undefined; + addon.uninstall(); + + Cu.import("resource://gre/modules/Services.jsm"); + for (var stage of [ "install", "startup", "shutdown", "uninstall" ]) { + for (var symbol of [ "IDBKeyRange", "indexedDB" ]) { + let pref; + try { + pref = Services.prefs.getBoolPref("indexeddbtest.bootstrap." + stage + + "." + symbol); + } + catch(ex) { + pref = false; + } + ok(pref, "Symbol '" + symbol + "' present during '" + stage + "'"); + } + } + + finishTest(); + yield undefined; + } + + window.runTest = function() { + SimpleTest.waitForExplicitFinish(); + + testGenerator.next(); + } + ]]> + </script> + + <script type="text/javascript;version=1.7" src="chromeHelpers.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=832883" + target="_blank">Mozilla Bug 832883</a> + </body> +</window> diff --git a/dom/indexedDB/test/test_global_data.html b/dom/indexedDB/test/test_global_data.html new file mode 100644 index 000000000..0a83b7428 --- /dev/null +++ b/dom/indexedDB/test/test_global_data.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_global_data.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_index_empty_keyPath.html b/dom/indexedDB/test/test_index_empty_keyPath.html new file mode 100644 index 000000000..2c67c0edf --- /dev/null +++ b/dom/indexedDB/test/test_index_empty_keyPath.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_index_empty_keyPath.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_index_getAll.html b/dom/indexedDB/test/test_index_getAll.html new file mode 100644 index 000000000..7db8c7078 --- /dev/null +++ b/dom/indexedDB/test/test_index_getAll.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_index_getAll.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_index_getAllObjects.html b/dom/indexedDB/test/test_index_getAllObjects.html new file mode 100644 index 000000000..fe7343ecc --- /dev/null +++ b/dom/indexedDB/test/test_index_getAllObjects.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_index_getAllObjects.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_index_object_cursors.html b/dom/indexedDB/test/test_index_object_cursors.html new file mode 100644 index 000000000..81f2d6ca5 --- /dev/null +++ b/dom/indexedDB/test/test_index_object_cursors.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_index_object_cursors.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_index_update_delete.html b/dom/indexedDB/test/test_index_update_delete.html new file mode 100644 index 000000000..5e7888fc9 --- /dev/null +++ b/dom/indexedDB/test/test_index_update_delete.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_index_update_delete.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_indexes.html b/dom/indexedDB/test/test_indexes.html new file mode 100644 index 000000000..7c7400db2 --- /dev/null +++ b/dom/indexedDB/test/test_indexes.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_indexes.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_indexes_bad_values.html b/dom/indexedDB/test/test_indexes_bad_values.html new file mode 100644 index 000000000..54479ad2c --- /dev/null +++ b/dom/indexedDB/test/test_indexes_bad_values.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_indexes_bad_values.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_indexes_funny_things.html b/dom/indexedDB/test/test_indexes_funny_things.html new file mode 100644 index 000000000..a253d5f11 --- /dev/null +++ b/dom/indexedDB/test/test_indexes_funny_things.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_indexes_funny_things.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_invalid_cursor.html b/dom/indexedDB/test/test_invalid_cursor.html new file mode 100644 index 000000000..778675725 --- /dev/null +++ b/dom/indexedDB/test/test_invalid_cursor.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>IndexedDB Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_invalid_cursor.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_invalid_version.html b/dom/indexedDB/test/test_invalid_version.html new file mode 100644 index 000000000..ec83665ac --- /dev/null +++ b/dom/indexedDB/test/test_invalid_version.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_invalid_version.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_invalidate.html b/dom/indexedDB/test/test_invalidate.html new file mode 100644 index 000000000..45651953c --- /dev/null +++ b/dom/indexedDB/test/test_invalidate.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>IndexedDB Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_invalidate.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_key_requirements.html b/dom/indexedDB/test/test_key_requirements.html new file mode 100644 index 000000000..4945403f4 --- /dev/null +++ b/dom/indexedDB/test/test_key_requirements.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_key_requirements.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_keys.html b/dom/indexedDB/test/test_keys.html new file mode 100644 index 000000000..4657ce9c0 --- /dev/null +++ b/dom/indexedDB/test/test_keys.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_keys.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_leaving_page.html b/dom/indexedDB/test/test_leaving_page.html new file mode 100644 index 000000000..6eb46f50e --- /dev/null +++ b/dom/indexedDB/test/test_leaving_page.html @@ -0,0 +1,49 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Leaving Page Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body onload="runTest();"> + <iframe id="inner"></iframe> + <a id="a" href="leaving_page_iframe.html"></a> + + <script type="text/javascript;version=1.7"> + onmessage = function(e) { + ok(false, "gotmessage: " + e.data); + } + + function testSteps() + { + var iframe = $("inner"); + iframe.src = "leaving_page_iframe.html"; + iframe.onload = continueToNextStep; + yield undefined; + is(iframe.contentWindow.location.href, $("a").href, + "should navigate to iframe page"); + yield undefined; + is(iframe.contentWindow.location.href, "about:blank", + "should nagivate to about:blank"); + + let request = indexedDB.open(location, 1); + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.transaction(["mystore"]).objectStore("mystore").get(42).onsuccess = + grabEventAndContinueHandler; + event = yield undefined; + is(event.target.result.hello, "world", "second modification rolled back"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</html> diff --git a/dom/indexedDB/test/test_locale_aware_index_getAll.html b/dom/indexedDB/test/test_locale_aware_index_getAll.html new file mode 100644 index 000000000..f7c62635d --- /dev/null +++ b/dom/indexedDB/test/test_locale_aware_index_getAll.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_locale_aware_index_getAll.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_locale_aware_index_getAllObjects.html b/dom/indexedDB/test/test_locale_aware_index_getAllObjects.html new file mode 100644 index 000000000..d5d8c5c54 --- /dev/null +++ b/dom/indexedDB/test/test_locale_aware_index_getAllObjects.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_locale_aware_index_getAllObjects.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_locale_aware_indexes.html b/dom/indexedDB/test/test_locale_aware_indexes.html new file mode 100644 index 000000000..994f6c6f7 --- /dev/null +++ b/dom/indexedDB/test/test_locale_aware_indexes.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_locale_aware_indexes.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_lowDiskSpace.html b/dom/indexedDB/test/test_lowDiskSpace.html new file mode 100644 index 000000000..cffd46549 --- /dev/null +++ b/dom/indexedDB/test/test_lowDiskSpace.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Low Disk Space Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_lowDiskSpace.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_maximal_serialized_object_size.html b/dom/indexedDB/test/test_maximal_serialized_object_size.html new file mode 100644 index 000000000..efb4e98e3 --- /dev/null +++ b/dom/indexedDB/test/test_maximal_serialized_object_size.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test Maximal Size of a Serialized Object</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_maximal_serialized_object_size.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_message_manager_ipc.html b/dom/indexedDB/test/test_message_manager_ipc.html new file mode 100644 index 000000000..f89cb9f89 --- /dev/null +++ b/dom/indexedDB/test/test_message_manager_ipc.html @@ -0,0 +1,343 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for sending IndexedDB Blobs through MessageManager</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body onload="setup();"> + <script type="application/javascript;version=1.7"> +"use strict"; + +function childFrameScript() { + "use strict"; + + const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + + const mmName = "test:idb-and-mm"; + + const dbName = "test_message_manager_ipc.html - CHILD"; + const dbVersion = 1; + const objStoreName = "bar"; + const key = 1; + + const blobData = ["So", " ", "many", " ", "blobs!"]; + const blobText = blobData.join(""); + const blobType = "text/plain"; + + Cu.importGlobalProperties(["indexedDB"]); + + function info(msg) { + sendAsyncMessage(mmName, { op: "info", msg: msg }); + } + + function ok(condition, name, diag) { + sendAsyncMessage(mmName, + { op: "ok", + condition: condition, + name: name, + diag: diag }); + } + + function is(a, b, name) { + let pass = a == b; + let diag = pass ? "" : "got " + a + ", expected " + b; + ok(pass, name, diag); + } + + function finish(result) { + sendAsyncMessage(mmName, { op: "done", result: result }); + } + + function grabAndContinue(arg) { + testGenerator.send(arg); + } + + function errorHandler(event) { + ok(false, + event.target + " received error event: '" + event.target.error.name + + "'"); + finish(); + } + + function testSteps() { + addMessageListener(mmName, grabAndContinue); + let message = yield undefined; + + let blob = message.data; + + ok(blob instanceof Ci.nsIDOMBlob, "Message manager sent a blob"); + is(blob.size, blobText.length, "Blob has correct length"); + is(blob.type, blobType, "Blob has correct type"); + + info("Reading blob"); + + let reader = new FileReader(); + reader.addEventListener("load", grabAndContinue); + reader.readAsText(blob); + + yield undefined; + + is(reader.result, blobText, "Blob has correct data"); + + let slice = blob.slice(0, blobData[0].length, blobType); + + ok(slice instanceof Ci.nsIDOMBlob, "Slice returned a blob"); + is(slice.size, blobData[0].length, "Slice has correct length"); + is(slice.type, blobType, "Slice has correct type"); + + info("Reading slice"); + + reader = new FileReader(); + reader.addEventListener("load", grabAndContinue); + reader.readAsText(slice); + + yield undefined; + + is(reader.result, blobData[0], "Slice has correct data"); + + info("Deleting database"); + + let req = indexedDB.deleteDatabase(dbName); + req.onerror = errorHandler; + req.onsuccess = grabAndContinue; + + let event = yield undefined; + is(event.type, "success", "Got success event"); + + info("Opening database"); + + req = indexedDB.open(dbName, dbVersion); + req.onerror = errorHandler; + req.onupgradeneeded = grabAndContinue; + req.onsuccess = grabAndContinue; + + event = yield undefined; + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + event.target.result.createObjectStore(objStoreName); + + event = yield undefined; + is(event.type, "success", "Got success event"); + + let db = event.target.result; + + info("Storing blob from message manager in database"); + + let objectStore = + db.transaction(objStoreName, "readwrite").objectStore(objStoreName); + req = objectStore.add(blob, key); + req.onerror = errorHandler; + req.onsuccess = grabAndContinue; + + event = yield undefined; + + info("Getting blob from database"); + + objectStore = db.transaction(objStoreName).objectStore(objStoreName); + req = objectStore.get(key); + req.onerror = errorHandler; + req.onsuccess = grabAndContinue; + + event = yield undefined; + + blob = event.target.result; + + ok(blob instanceof Ci.nsIDOMBlob, "Database gave us a blob"); + is(blob.size, blobText.length, "Blob has correct length"); + is(blob.type, blobType, "Blob has correct type"); + + info("Reading blob"); + + reader = new FileReader(); + reader.addEventListener("load", grabAndContinue); + reader.readAsText(blob); + + yield undefined; + + is(reader.result, blobText, "Blob has correct data"); + + info("Storing slice from message manager in database"); + + objectStore = + db.transaction(objStoreName, "readwrite").objectStore(objStoreName); + req = objectStore.put(slice, key); + req.onerror = errorHandler; + req.onsuccess = grabAndContinue; + + event = yield undefined; + + info("Getting slice from database"); + + objectStore = db.transaction(objStoreName).objectStore(objStoreName); + req = objectStore.get(key); + req.onerror = errorHandler; + req.onsuccess = grabAndContinue; + + event = yield undefined; + + slice = event.target.result; + + ok(slice instanceof Ci.nsIDOMBlob, "Database gave us a blob"); + is(slice.size, blobData[0].length, "Slice has correct length"); + is(slice.type, blobType, "Slice has correct type"); + + info("Reading Slice"); + + reader = new FileReader(); + reader.addEventListener("load", grabAndContinue); + reader.readAsText(slice); + + yield undefined; + + is(reader.result, blobData[0], "Slice has correct data"); + + info("Sending blob and slice from database to message manager"); + finish([blob, slice]); + + yield undefined; + } + + let testGenerator = testSteps(); + testGenerator.next(); +} + +function parentFrameScript(mm) { + const messageName = "test:idb-and-mm"; + const blobData = ["So", " ", "many", " ", "blobs!"]; + const blobText = blobData.join(""); + const blobType = "text/plain"; + const blob = new Blob(blobData, { type: blobType }); + + function grabAndContinue(arg) { + testGenerator.send(arg); + } + + function testSteps() { + let result = yield undefined; + + is(Array.isArray(result), true, "Child delivered an array of results"); + is(result.length, 2, "Child delivered two results"); + + let blob = result[0]; + is(blob instanceof Blob, true, "Child delivered a blob"); + is(blob.size, blobText.length, "Blob has correct size"); + is(blob.type, blobType, "Blob has correct type"); + + let slice = result[1]; + is(slice instanceof Blob, true, "Child delivered a slice"); + is(slice.size, blobData[0].length, "Slice has correct size"); + is(slice.type, blobType, "Slice has correct type"); + + info("Reading blob"); + + let reader = new FileReader(); + reader.onload = grabAndContinue; + reader.readAsText(blob); + yield undefined; + + is(reader.result, blobText, "Blob has correct data"); + + info("Reading slice"); + + reader = new FileReader(); + reader.onload = grabAndContinue; + reader.readAsText(slice); + yield undefined; + + is(reader.result, blobData[0], "Slice has correct data"); + + slice = blob.slice(0, blobData[0].length, blobType); + + is(slice instanceof Blob, true, "Made a new slice from blob"); + is(slice.size, blobData[0].length, "Second slice has correct size"); + is(slice.type, blobType, "Second slice has correct type"); + + info("Reading second slice"); + + reader = new FileReader(); + reader.onload = grabAndContinue; + reader.readAsText(slice); + yield undefined; + + is(reader.result, blobData[0], "Second slice has correct data"); + + SimpleTest.finish(); + yield undefined; + } + + let testGenerator = testSteps(); + testGenerator.next(); + + mm.addMessageListener(messageName, function(message) { + let data = message.data; + switch (data.op) { + case "info": { + info(data.msg); + break; + } + + case "ok": { + ok(data.condition, data.name, data.diag); + break; + } + + case "done": { + testGenerator.send(data.result); + break; + } + + default: { + ok(false, "Unknown op: " + data.op); + SimpleTest.finish(); + } + } + }); + + mm.loadFrameScript("data:,(" + childFrameScript.toString() + ")();", + false); + + mm.sendAsyncMessage(messageName, blob); +} + +function setup() { + info("Got load event"); + + SpecialPowers.pushPrefEnv( + { set: [ ["dom.ipc.browser_frames.oop_by_default", true], + ["dom.mozBrowserFramesEnabled", true], + ["network.disable.ipc.security", true], + ["browser.pagethumbnails.capturing_disabled", true] ] }, + function() { + info("Prefs set"); + + SpecialPowers.pushPermissions( + [ { type: "browser", allow: true, context: document } ], + function() { + info("Permissions set"); + + let iframe = document.createElement("iframe"); + SpecialPowers.wrap(iframe).mozbrowser = true; + iframe.id = "iframe"; + iframe.src = + "data:text/html,<!DOCTYPE HTML><html><body></body></html>"; + + iframe.addEventListener("mozbrowserloadend", function() { + info("Starting tests"); + + let mm = SpecialPowers.getBrowserFrameMessageManager(iframe) + parentFrameScript(mm); + }); + + document.body.appendChild(iframe); + } + ); + } + ); +} + +SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/indexedDB/test/test_multientry.html b/dom/indexedDB/test/test_multientry.html new file mode 100644 index 000000000..8523e30b5 --- /dev/null +++ b/dom/indexedDB/test/test_multientry.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_multientry.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_names_sorted.html b/dom/indexedDB/test/test_names_sorted.html new file mode 100644 index 000000000..42bffbc14 --- /dev/null +++ b/dom/indexedDB/test/test_names_sorted.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_names_sorted.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_objectCursors.html b/dom/indexedDB/test/test_objectCursors.html new file mode 100644 index 000000000..e05634a2a --- /dev/null +++ b/dom/indexedDB/test/test_objectCursors.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_objectCursors.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_objectStore_getAllKeys.html b/dom/indexedDB/test/test_objectStore_getAllKeys.html new file mode 100644 index 000000000..7ae98882d --- /dev/null +++ b/dom/indexedDB/test/test_objectStore_getAllKeys.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_objectStore_getAllKeys.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_objectStore_inline_autoincrement_key_added_on_put.html b/dom/indexedDB/test/test_objectStore_inline_autoincrement_key_added_on_put.html new file mode 100644 index 000000000..a66147232 --- /dev/null +++ b/dom/indexedDB/test/test_objectStore_inline_autoincrement_key_added_on_put.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_objectStore_inline_autoincrement_key_added_on_put.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_objectStore_openKeyCursor.html b/dom/indexedDB/test/test_objectStore_openKeyCursor.html new file mode 100644 index 000000000..4c6437071 --- /dev/null +++ b/dom/indexedDB/test/test_objectStore_openKeyCursor.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_objectStore_openKeyCursor.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_objectStore_remove_values.html b/dom/indexedDB/test/test_objectStore_remove_values.html new file mode 100644 index 000000000..a18349236 --- /dev/null +++ b/dom/indexedDB/test/test_objectStore_remove_values.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_objectStore_remove_values.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_object_identity.html b/dom/indexedDB/test/test_object_identity.html new file mode 100644 index 000000000..1eda2e7f0 --- /dev/null +++ b/dom/indexedDB/test/test_object_identity.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_object_identity.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_odd_result_order.html b/dom/indexedDB/test/test_odd_result_order.html new file mode 100644 index 000000000..b95315343 --- /dev/null +++ b/dom/indexedDB/test/test_odd_result_order.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_odd_result_order.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_open_empty_db.html b/dom/indexedDB/test/test_open_empty_db.html new file mode 100644 index 000000000..84a16733a --- /dev/null +++ b/dom/indexedDB/test/test_open_empty_db.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_open_empty_db.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_open_for_principal.html b/dom/indexedDB/test/test_open_for_principal.html new file mode 100644 index 000000000..96bc2c43e --- /dev/null +++ b/dom/indexedDB/test/test_open_for_principal.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + is("open" in indexedDB, true, "open() defined"); + is("openForPrincipal" in indexedDB, false, "openForPrincipal() not defined"); + + is("deleteDatabase" in indexedDB, true, "deleteDatabase() defined"); + is("deleteForPrincipal" in indexedDB, false, "deleteForPrincipal() not defined"); + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_open_objectStore.html b/dom/indexedDB/test/test_open_objectStore.html new file mode 100644 index 000000000..f83fd9e92 --- /dev/null +++ b/dom/indexedDB/test/test_open_objectStore.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_open_objectStore.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_optionalArguments.html b/dom/indexedDB/test/test_optionalArguments.html new file mode 100644 index 000000000..ae15f2da2 --- /dev/null +++ b/dom/indexedDB/test/test_optionalArguments.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_optionalArguments.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_overlapping_transactions.html b/dom/indexedDB/test/test_overlapping_transactions.html new file mode 100644 index 000000000..6371b04bc --- /dev/null +++ b/dom/indexedDB/test/test_overlapping_transactions.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_overlapping_transactions.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_persistenceType.html b/dom/indexedDB/test/test_persistenceType.html new file mode 100644 index 000000000..cc44ffb77 --- /dev/null +++ b/dom/indexedDB/test/test_persistenceType.html @@ -0,0 +1,93 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + function testSteps() + { + const name = window.location.pathname; + const version = 1; + const storages = ["persistent", "temporary", "default"]; + + const objectStoreName = "Foo"; + const data = { key: 1, value: "bar" }; + + try { + indexedDB.open(name, { version: version, storage: "unknown" }); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof TypeError, "Got TypeError."); + is(e.name, "TypeError", "Good error name."); + } + + for (let storage of storages) { + let request = indexedDB.open(name, { version: version, + storage: storage }); + + if (storage == "persistent" && + SpecialPowers.Services.appinfo.widgetToolkit == "android") { + request.onerror = expectedErrorHandler("InvalidStateError"); + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "error", "Got corrent event type"); + + continue; + } + + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + is(db.name, name, "Correct name"); + is(db.version, version, "Correct version"); + is(db.storage, storage, "Correct persistence type"); + + objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + request = objectStore.get(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Got no data"); + + request = objectStore.add(data.value, data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.key, "Got correct key"); + } + + finishTest(); + yield undefined; + } + </script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_put_get_values.html b/dom/indexedDB/test/test_put_get_values.html new file mode 100644 index 000000000..57bee9347 --- /dev/null +++ b/dom/indexedDB/test/test_put_get_values.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_put_get_values.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_put_get_values_autoIncrement.html b/dom/indexedDB/test/test_put_get_values_autoIncrement.html new file mode 100644 index 000000000..d468634a2 --- /dev/null +++ b/dom/indexedDB/test/test_put_get_values_autoIncrement.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_put_get_values_autoIncrement.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_readonly_transactions.html b/dom/indexedDB/test/test_readonly_transactions.html new file mode 100644 index 000000000..c6abd48ce --- /dev/null +++ b/dom/indexedDB/test/test_readonly_transactions.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_readonly_transactions.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_readwriteflush_disabled.html b/dom/indexedDB/test/test_readwriteflush_disabled.html new file mode 100644 index 000000000..08523317a --- /dev/null +++ b/dom/indexedDB/test/test_readwriteflush_disabled.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_readwriteflush_disabled.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_remove_index.html b/dom/indexedDB/test/test_remove_index.html new file mode 100644 index 000000000..7e52615d3 --- /dev/null +++ b/dom/indexedDB/test/test_remove_index.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_remove_index.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_remove_objectStore.html b/dom/indexedDB/test/test_remove_objectStore.html new file mode 100644 index 000000000..41bcf87c6 --- /dev/null +++ b/dom/indexedDB/test/test_remove_objectStore.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_remove_objectStore.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_rename_index.html b/dom/indexedDB/test/test_rename_index.html new file mode 100644 index 000000000..dfd77114c --- /dev/null +++ b/dom/indexedDB/test/test_rename_index.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_rename_index.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_rename_index_errors.html b/dom/indexedDB/test/test_rename_index_errors.html new file mode 100644 index 000000000..44a48be7b --- /dev/null +++ b/dom/indexedDB/test/test_rename_index_errors.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_rename_index_errors.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_rename_objectStore.html b/dom/indexedDB/test/test_rename_objectStore.html new file mode 100644 index 000000000..85cbb0cee --- /dev/null +++ b/dom/indexedDB/test/test_rename_objectStore.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_rename_objectStore.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_rename_objectStore_errors.html b/dom/indexedDB/test/test_rename_objectStore_errors.html new file mode 100644 index 000000000..e98fc8d11 --- /dev/null +++ b/dom/indexedDB/test/test_rename_objectStore_errors.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_rename_objectStore_errors.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_request_readyState.html b/dom/indexedDB/test/test_request_readyState.html new file mode 100644 index 000000000..8a21131d7 --- /dev/null +++ b/dom/indexedDB/test/test_request_readyState.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_request_readyState.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_sandbox.html b/dom/indexedDB/test/test_sandbox.html new file mode 100644 index 000000000..a6c627fb1 --- /dev/null +++ b/dom/indexedDB/test/test_sandbox.html @@ -0,0 +1,101 @@ +<!doctype html> +<html> +<head> + <title>indexedDB in JS Sandbox</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"></link> +</head> +<body> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// This runs inside a same-origin sandbox. +// The intent being to show that the data store is the same. +function storeValue() { + function createDB_inner() { + var op = indexedDB.open('db'); + op.onupgradeneeded = e => { + var db = e.target.result; + db.createObjectStore('store'); + }; + return new Promise(resolve => { + op.onsuccess = e => resolve(e.target.result); + }); + } + + function add(k, v) { + return createDB_inner().then(db => { + var tx = db.transaction('store', 'readwrite'); + var store = tx.objectStore('store'); + var op = store.add(v, k); + return new Promise((resolve, reject) => { + op.onsuccess = e => resolve(e.target.result); + op.onerror = _ => reject(op.error); + tx.onabort = _ => reject(tx.error); + }); + }); + } + + return add('x', [ 10, {} ]) + .then(_ => step_done(), + _ => ok(false, 'failed to store')); +} + +function createDB_outer() { + var op = indexedDB.open('db'); + op.onupgradeneeded = e => { + ok(false, 'upgrade should not be needed'); + var db = e.target.result; + db.createObjectStore('store'); + }; + return new Promise(resolve => { + op.onsuccess = e => resolve(e.target.result); + }); +} + +function get(k) { + return createDB_outer().then(db => { + var tx = db.transaction('store', 'readonly'); + var store = tx.objectStore('store'); + var op = store.get(k); + return new Promise((resolve, reject) => { + op.onsuccess = e => resolve(e.target.result); + op.onerror = _ => reject(op.error); + tx.onabort = _ => reject(tx.error); + }); + }); +} + +function runInSandbox(sandbox, testFunc) { + is(typeof testFunc, 'function'); + var resolvePromise; + var testPromise = new Promise(r => resolvePromise = r); + SpecialPowers.Cu.exportFunction(_ => resolvePromise(), sandbox, + { defineAs: 'step_done' }); + SpecialPowers.Cu.evalInSandbox('(' + testFunc.toSource() + ')()' + + '.then(step_done);', sandbox); + return testPromise; +} + +// Use the window principal for the sandbox; location.origin is not sufficient. +var sb = new SpecialPowers.Cu.Sandbox(window, + { wantGlobalProperties: ['indexedDB'] }); + +sb.ok = SpecialPowers.Cu.exportFunction(ok, sb); + +Promise.resolve() + .then(_ => runInSandbox(sb, storeValue)) + .then(_ => get('x')) + .then(x => { + ok(x, 'a value should be present'); + is(x.length, 2); + is(x[0], 10); + is(typeof x[1], 'object'); + is(Object.keys(x[1]).length, 0); + }) + .then(_ => SimpleTest.finish()); + +</script> +</body> +</html> diff --git a/dom/indexedDB/test/test_serviceworker.html b/dom/indexedDB/test/test_serviceworker.html new file mode 100644 index 000000000..c37a70ffa --- /dev/null +++ b/dom/indexedDB/test/test_serviceworker.html @@ -0,0 +1,78 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1137245 - Allow IndexedDB usage in ServiceWorkers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var regisration; + function simpleRegister() { + return navigator.serviceWorker.register("service_worker.js", { + scope: 'service_worker_client.html' + }); + } + + function unregister() { + return registration.unregister(); + } + + function testIndexedDBAvailable(sw) { + registration = sw; + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + sw.active.postMessage("GO"); + return; + } + + if (!("available" in e.data)) { + ok(false, "Something went wrong"); + reject(); + return; + } + + ok(e.data.available, "IndexedDB available in service worker."); + resolve(); + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "service_worker_client.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + simpleRegister() + .then(testIndexedDBAvailable) + .then(unregister) + .then(SimpleTest.finish) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/indexedDB/test/test_setVersion.html b/dom/indexedDB/test/test_setVersion.html new file mode 100644 index 000000000..6af88b063 --- /dev/null +++ b/dom/indexedDB/test/test_setVersion.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_setVersion.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_setVersion_abort.html b/dom/indexedDB/test/test_setVersion_abort.html new file mode 100644 index 000000000..6037148b7 --- /dev/null +++ b/dom/indexedDB/test/test_setVersion_abort.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_setVersion_abort.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_setVersion_events.html b/dom/indexedDB/test/test_setVersion_events.html new file mode 100644 index 000000000..79a982844 --- /dev/null +++ b/dom/indexedDB/test/test_setVersion_events.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_setVersion_events.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_setVersion_exclusion.html b/dom/indexedDB/test/test_setVersion_exclusion.html new file mode 100644 index 000000000..22b645fcc --- /dev/null +++ b/dom/indexedDB/test/test_setVersion_exclusion.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_setVersion_exclusion.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_setVersion_throw.html b/dom/indexedDB/test/test_setVersion_throw.html new file mode 100644 index 000000000..015341b0c --- /dev/null +++ b/dom/indexedDB/test/test_setVersion_throw.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_setVersion_throw.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_storage_manager_estimate.html b/dom/indexedDB/test/test_storage_manager_estimate.html new file mode 100644 index 000000000..1ba36ecef --- /dev/null +++ b/dom/indexedDB/test/test_storage_manager_estimate.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test for StorageManager</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_storage_manager_estimate.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="setup();"></body> + +</html> diff --git a/dom/indexedDB/test/test_success_events_after_abort.html b/dom/indexedDB/test/test_success_events_after_abort.html new file mode 100644 index 000000000..624cc75cc --- /dev/null +++ b/dom/indexedDB/test/test_success_events_after_abort.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_success_events_after_abort.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_table_locks.html b/dom/indexedDB/test/test_table_locks.html new file mode 100644 index 000000000..245d79eb4 --- /dev/null +++ b/dom/indexedDB/test/test_table_locks.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>IndexedDB Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_table_locks.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_table_rollback.html b/dom/indexedDB/test/test_table_rollback.html new file mode 100644 index 000000000..4c8664f7e --- /dev/null +++ b/dom/indexedDB/test/test_table_rollback.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_table_rollback.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_third_party.html b/dom/indexedDB/test/test_third_party.html new file mode 100644 index 000000000..91ea44fc3 --- /dev/null +++ b/dom/indexedDB/test/test_third_party.html @@ -0,0 +1,103 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7"> + const BEHAVIOR_ACCEPT = 0; + const BEHAVIOR_REJECTFOREIGN = 1; + const BEHAVIOR_REJECT = 2; + const BEHAVIOR_LIMITFOREIGN = 3; + + const testData = [ + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_ACCEPT, expectedResult: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_ACCEPT, expectedResult: true }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_ACCEPT, expectedResult: true }, + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_ACCEPT, expectedResult: true }, + + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECT, expectedResult: false }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_REJECT, expectedResult: false }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECT, expectedResult: false }, + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECT, expectedResult: false }, + + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResult: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResult: false }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResult: false }, + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResult: true }, + + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResult: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResult: false }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResult: false }, + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResult: true } + ]; + + const iframe1Path = + window.location.pathname.replace("test_third_party.html", + "third_party_iframe1.html"); + const iframe2URL = + "http://" + window.location.host + + window.location.pathname.replace("test_third_party.html", + "third_party_iframe2.html"); + + let testIndex = 0; + let testRunning = false; + + function iframeLoaded() { + let message = { source: "parent", href: iframe2URL }; + let iframe = document.getElementById("iframe1"); + iframe.contentWindow.postMessage(message.toSource(), "*"); + } + + function setiframe() { + let iframe = document.getElementById("iframe1"); + + if (!testRunning) { + testRunning = true; + iframe.addEventListener("load", iframeLoaded, false); + } + SpecialPowers.pushPrefEnv({ + 'set': [["network.cookie.cookieBehavior", testData[testIndex].cookieBehavior]] + }, () => { + iframe.src = testData[testIndex].host + iframe1Path; + }); + // SpecialPowers.setIntPref("network.cookie.cookieBehavior", testData[testIndex].cookieBehavior); + } + + function messageListener(event) { + let message = eval(event.data); + + is(message.source, "iframe", "Good source"); + is(message.result, testData[testIndex].expectedResult, "Good result"); + + if (testIndex < testData.length - 1) { + testIndex++; + setiframe(); + return; + } + + SimpleTest.finish(); + } + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.addPermission("indexedDB", true, document); + + window.addEventListener("message", messageListener, false); + setiframe(); + } + </script> + +</head> + +<body onload="runTest();"> + <iframe id="iframe1"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/test_traffic_jam.html b/dom/indexedDB/test/test_traffic_jam.html new file mode 100644 index 000000000..8973b5021 --- /dev/null +++ b/dom/indexedDB/test/test_traffic_jam.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_traffic_jam.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_transaction_abort.html b/dom/indexedDB/test/test_transaction_abort.html new file mode 100644 index 000000000..849d43938 --- /dev/null +++ b/dom/indexedDB/test/test_transaction_abort.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_abort.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_transaction_abort_hang.html b/dom/indexedDB/test/test_transaction_abort_hang.html new file mode 100644 index 000000000..3a0fcfb4e --- /dev/null +++ b/dom/indexedDB/test/test_transaction_abort_hang.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_abort_hang.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_transaction_duplicate_store_names.html b/dom/indexedDB/test/test_transaction_duplicate_store_names.html new file mode 100644 index 000000000..c7187521e --- /dev/null +++ b/dom/indexedDB/test/test_transaction_duplicate_store_names.html @@ -0,0 +1,16 @@ +<!-- +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for Bug 1013221</title> + + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_duplicate_store_names.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> +<body onload="runTest();"></body> +</html> diff --git a/dom/indexedDB/test/test_transaction_error.html b/dom/indexedDB/test/test_transaction_error.html new file mode 100644 index 000000000..39a05ae35 --- /dev/null +++ b/dom/indexedDB/test/test_transaction_error.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_error.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_transaction_lifetimes.html b/dom/indexedDB/test/test_transaction_lifetimes.html new file mode 100644 index 000000000..0917f74e7 --- /dev/null +++ b/dom/indexedDB/test/test_transaction_lifetimes.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_lifetimes.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_transaction_lifetimes_nested.html b/dom/indexedDB/test/test_transaction_lifetimes_nested.html new file mode 100644 index 000000000..5f22c4ed5 --- /dev/null +++ b/dom/indexedDB/test/test_transaction_lifetimes_nested.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_lifetimes_nested.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_transaction_ordering.html b/dom/indexedDB/test/test_transaction_ordering.html new file mode 100644 index 000000000..ff5e6a6ed --- /dev/null +++ b/dom/indexedDB/test/test_transaction_ordering.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_transaction_ordering.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_unique_index_update.html b/dom/indexedDB/test/test_unique_index_update.html new file mode 100644 index 000000000..03d00a459 --- /dev/null +++ b/dom/indexedDB/test/test_unique_index_update.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_unique_index_update.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_view_put_get_values.html b/dom/indexedDB/test/test_view_put_get_values.html new file mode 100644 index 000000000..e98bf0b52 --- /dev/null +++ b/dom/indexedDB/test/test_view_put_get_values.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_view_put_get_values.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_wasm_cursors.html b/dom/indexedDB/test/test_wasm_cursors.html new file mode 100644 index 000000000..dabf1b101 --- /dev/null +++ b/dom/indexedDB/test/test_wasm_cursors.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_wasm_cursors.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_wasm_getAll.html b/dom/indexedDB/test/test_wasm_getAll.html new file mode 100644 index 000000000..ba3539a9f --- /dev/null +++ b/dom/indexedDB/test/test_wasm_getAll.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_wasm_getAll.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_wasm_index_getAllObjects.html b/dom/indexedDB/test/test_wasm_index_getAllObjects.html new file mode 100644 index 000000000..a2f812835 --- /dev/null +++ b/dom/indexedDB/test/test_wasm_index_getAllObjects.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_wasm_index_getAllObjects.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_wasm_indexes.html b/dom/indexedDB/test/test_wasm_indexes.html new file mode 100644 index 000000000..fe9f2071d --- /dev/null +++ b/dom/indexedDB/test/test_wasm_indexes.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_wasm_indexes.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_wasm_put_get_values.html b/dom/indexedDB/test/test_wasm_put_get_values.html new file mode 100644 index 000000000..5769ffdc2 --- /dev/null +++ b/dom/indexedDB/test/test_wasm_put_get_values.html @@ -0,0 +1,20 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_wasm_put_get_values.js"></script> + <script type="text/javascript;version=1.7" src="file.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_writer_starvation.html b/dom/indexedDB/test/test_writer_starvation.html new file mode 100644 index 000000000..857721747 --- /dev/null +++ b/dom/indexedDB/test/test_writer_starvation.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript;version=1.7" src="unit/test_writer_starvation.js"></script> + <script type="text/javascript;version=1.7" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/third_party_iframe1.html b/dom/indexedDB/test/third_party_iframe1.html new file mode 100644 index 000000000..c828acc37 --- /dev/null +++ b/dom/indexedDB/test/third_party_iframe1.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + function messageListener(event) { + let message = eval(event.data); + + if (message.source == "parent") { + document.getElementById("iframe2").src = message.href; + } + else if (message.source == "iframe") { + parent.postMessage(event.data, "*"); + } + } + </script> + +</head> + +<body onload="window.addEventListener('message', messageListener, false);"> + <iframe id="iframe2"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/third_party_iframe2.html b/dom/indexedDB/test/third_party_iframe2.html new file mode 100644 index 000000000..bcb260819 --- /dev/null +++ b/dom/indexedDB/test/third_party_iframe2.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script type="text/javascript;version=1.7"> + function report(result) { + let message = { source: "iframe" }; + message.result = result; + window.parent.postMessage(message.toSource(), "*"); + } + + function runIndexedDBTest() { + try { + let request = indexedDB.open(window.location.pathname, 1); + request.onsuccess = function(event) { + report(!!(event.target.result instanceof IDBDatabase)); + } + } + catch (e) { + report(false); + } + } + </script> + +</head> + +<body onload="runIndexedDBTest();"> +</body> + +</html> diff --git a/dom/indexedDB/test/unit/GlobalObjectsChild.js b/dom/indexedDB/test/unit/GlobalObjectsChild.js new file mode 100644 index 000000000..5351ff2f1 --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsChild.js @@ -0,0 +1,38 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function ok(cond, msg) { + dump("ok(" + cond + ", \"" + msg + "\")"); + do_check_true(!!cond, Components.stack.caller); +} + +function finishTest() +{ + do_execute_soon(function() { + do_test_finished(); + }); +} + +function run_test() { + const name = "Splendid Test"; + + Cu.importGlobalProperties(["indexedDB"]); + + do_test_pending(); + + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + finishTest(); + } + request.onsuccess = function(event) { + let db = event.target.result; + ok(db, "Got database"); + finishTest(); + } +} diff --git a/dom/indexedDB/test/unit/GlobalObjectsComponent.js b/dom/indexedDB/test/unit/GlobalObjectsComponent.js new file mode 100644 index 000000000..44bc1afe9 --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsComponent.js @@ -0,0 +1,43 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.importGlobalProperties(["indexedDB"]); + +function GlobalObjectsComponent() { + this.wrappedJSObject = this; +} + +GlobalObjectsComponent.prototype = +{ + classID: Components.ID("{949ebf50-e0da-44b9-8335-cbfd4febfdcc}"), + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports]), + + runTest: function() { + const name = "Splendid Test"; + + let ok = this.ok; + let finishTest = this.finishTest; + + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + finishTest(); + } + request.onsuccess = function(event) { + let db = event.target.result; + ok(db, "Got database"); + finishTest(); + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([GlobalObjectsComponent]); diff --git a/dom/indexedDB/test/unit/GlobalObjectsComponent.manifest b/dom/indexedDB/test/unit/GlobalObjectsComponent.manifest new file mode 100644 index 000000000..be0a28bc1 --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsComponent.manifest @@ -0,0 +1,2 @@ +component {949ebf50-e0da-44b9-8335-cbfd4febfdcc} GlobalObjectsComponent.js +contract @mozilla.org/dom/indexeddb/GlobalObjectsComponent;1 {949ebf50-e0da-44b9-8335-cbfd4febfdcc} diff --git a/dom/indexedDB/test/unit/GlobalObjectsModule.jsm b/dom/indexedDB/test/unit/GlobalObjectsModule.jsm new file mode 100644 index 000000000..fe214f722 --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsModule.jsm @@ -0,0 +1,36 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Components.utils.importGlobalProperties(["indexedDB"]); + +this.EXPORTED_SYMBOLS = [ + "GlobalObjectsModule" +]; + +this.GlobalObjectsModule = function GlobalObjectsModule() { +} + +GlobalObjectsModule.prototype = { + runTest: function() { + const name = "Splendid Test"; + + let ok = this.ok; + let finishTest = this.finishTest; + + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + finishTest(); + } + request.onsuccess = function(event) { + let db = event.target.result; + ok(db, "Got database"); + finishTest(); + } + } +} diff --git a/dom/indexedDB/test/unit/GlobalObjectsSandbox.js b/dom/indexedDB/test/unit/GlobalObjectsSandbox.js new file mode 100644 index 000000000..094510271 --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsSandbox.js @@ -0,0 +1,22 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function runTest() { + const name = "Splendid Test"; + + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = function(event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + finishTest(); + } + request.onsuccess = function(event) { + let db = event.target.result; + ok(db, "Got database"); + finishTest(); + } +} diff --git a/dom/indexedDB/test/unit/bug1056939_profile.zip b/dom/indexedDB/test/unit/bug1056939_profile.zip Binary files differnew file mode 100644 index 000000000..db3cfe624 --- /dev/null +++ b/dom/indexedDB/test/unit/bug1056939_profile.zip diff --git a/dom/indexedDB/test/unit/defaultStorageUpgrade_profile.zip b/dom/indexedDB/test/unit/defaultStorageUpgrade_profile.zip Binary files differnew file mode 100644 index 000000000..68bb4749f --- /dev/null +++ b/dom/indexedDB/test/unit/defaultStorageUpgrade_profile.zip diff --git a/dom/indexedDB/test/unit/getUsage_profile.zip b/dom/indexedDB/test/unit/getUsage_profile.zip Binary files differnew file mode 100644 index 000000000..e484edd0c --- /dev/null +++ b/dom/indexedDB/test/unit/getUsage_profile.zip diff --git a/dom/indexedDB/test/unit/idbSubdirUpgrade1_profile.zip b/dom/indexedDB/test/unit/idbSubdirUpgrade1_profile.zip Binary files differnew file mode 100644 index 000000000..b7e434171 --- /dev/null +++ b/dom/indexedDB/test/unit/idbSubdirUpgrade1_profile.zip diff --git a/dom/indexedDB/test/unit/idbSubdirUpgrade2_profile.zip b/dom/indexedDB/test/unit/idbSubdirUpgrade2_profile.zip Binary files differnew file mode 100644 index 000000000..264c68d86 --- /dev/null +++ b/dom/indexedDB/test/unit/idbSubdirUpgrade2_profile.zip diff --git a/dom/indexedDB/test/unit/metadata2Restore_profile.zip b/dom/indexedDB/test/unit/metadata2Restore_profile.zip Binary files differnew file mode 100644 index 000000000..e5302b36c --- /dev/null +++ b/dom/indexedDB/test/unit/metadata2Restore_profile.zip diff --git a/dom/indexedDB/test/unit/metadataRestore_profile.zip b/dom/indexedDB/test/unit/metadataRestore_profile.zip Binary files differnew file mode 100644 index 000000000..a01d49166 --- /dev/null +++ b/dom/indexedDB/test/unit/metadataRestore_profile.zip diff --git a/dom/indexedDB/test/unit/mutableFileUpgrade_profile.zip b/dom/indexedDB/test/unit/mutableFileUpgrade_profile.zip Binary files differnew file mode 100644 index 000000000..4c89acf0a --- /dev/null +++ b/dom/indexedDB/test/unit/mutableFileUpgrade_profile.zip diff --git a/dom/indexedDB/test/unit/oldDirectories_profile.zip b/dom/indexedDB/test/unit/oldDirectories_profile.zip Binary files differnew file mode 100644 index 000000000..09209d351 --- /dev/null +++ b/dom/indexedDB/test/unit/oldDirectories_profile.zip diff --git a/dom/indexedDB/test/unit/schema18upgrade_profile.zip b/dom/indexedDB/test/unit/schema18upgrade_profile.zip Binary files differnew file mode 100644 index 000000000..e13cce9d2 --- /dev/null +++ b/dom/indexedDB/test/unit/schema18upgrade_profile.zip diff --git a/dom/indexedDB/test/unit/schema21upgrade_profile.zip b/dom/indexedDB/test/unit/schema21upgrade_profile.zip Binary files differnew file mode 100644 index 000000000..d08f88ea5 --- /dev/null +++ b/dom/indexedDB/test/unit/schema21upgrade_profile.zip diff --git a/dom/indexedDB/test/unit/schema23upgrade_profile.zip b/dom/indexedDB/test/unit/schema23upgrade_profile.zip Binary files differnew file mode 100644 index 000000000..888d24434 --- /dev/null +++ b/dom/indexedDB/test/unit/schema23upgrade_profile.zip diff --git a/dom/indexedDB/test/unit/snappyUpgrade_profile.zip b/dom/indexedDB/test/unit/snappyUpgrade_profile.zip Binary files differnew file mode 100644 index 000000000..f9635fc9f --- /dev/null +++ b/dom/indexedDB/test/unit/snappyUpgrade_profile.zip diff --git a/dom/indexedDB/test/unit/storagePersistentUpgrade_profile.zip b/dom/indexedDB/test/unit/storagePersistentUpgrade_profile.zip Binary files differnew file mode 100644 index 000000000..b1082106b --- /dev/null +++ b/dom/indexedDB/test/unit/storagePersistentUpgrade_profile.zip diff --git a/dom/indexedDB/test/unit/test_abort_deleted_index.js b/dom/indexedDB/test/unit/test_abort_deleted_index.js new file mode 100644 index 000000000..8bd1f6ae2 --- /dev/null +++ b/dom/indexedDB/test/unit/test_abort_deleted_index.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const storeName = "test store"; + const indexName_ToBeDeleted = "test index to be deleted"; + + info("Create index in v1."); + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore = db.createObjectStore(storeName, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStore.name, "Correct object store name"); + + // create index to be deleted later in v2. + objectStore.createIndex(indexName_ToBeDeleted, "foo"); + ok(objectStore.index(indexName_ToBeDeleted), "Index created."); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Delete index in v2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + objectStore = txn.objectStore(storeName); + let index = objectStore.index(indexName_ToBeDeleted); + ok(index, "index is valid."); + objectStore.deleteIndex(indexName_ToBeDeleted); + + // Aborting the transaction. + request.onerror = expectedErrorHandler("AbortError"); + txn.abort(); + try { + index.get('foo'); + ok(false, "TransactionInactiveError shall be thrown right after a deletion of an index is aborted."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TransactionInactiveError", "TransactionInactiveError shall be thrown right after a deletion of an index is aborted."); + } + + yield undefined; + + try { + index.get('foo'); + ok(false, "TransactionInactiveError shall be thrown after the transaction is inactive."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TransactionInactiveError", "TransactionInactiveError shall be thrown after the transaction is inactive."); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_abort_deleted_objectStore.js b/dom/indexedDB/test/unit/test_abort_deleted_objectStore.js new file mode 100644 index 000000000..98035b3da --- /dev/null +++ b/dom/indexedDB/test/unit/test_abort_deleted_objectStore.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const storeName_ToBeDeleted = "test store to be deleted"; + + info("Create objectStore in v1."); + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + // create objectstore to be deleted later in v2. + db.createObjectStore(storeName_ToBeDeleted, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_ToBeDeleted), "Correct name"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Delete objectStore in v2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + let objectStore = txn.objectStore(storeName_ToBeDeleted); + ok(objectStore, "objectStore is available"); + + db.deleteObjectStore(storeName_ToBeDeleted); + + // Aborting the transaction. + request.onerror = expectedErrorHandler("AbortError"); + txn.abort(); + try { + objectStore.get('foo'); + ok(false, "TransactionInactiveError shall be thrown if the transaction is inactive."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TransactionInactiveError", "correct error"); + } + + yield undefined; + + try { + objectStore.get('foo'); + ok(false, "TransactionInactiveError shall be thrown if the transaction is inactive."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TransactionInactiveError", "correct error"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_add_put.js b/dom/indexedDB/test/unit/test_add_put.js new file mode 100644 index 000000000..923d19d7e --- /dev/null +++ b/dom/indexedDB/test/unit/test_add_put.js @@ -0,0 +1,165 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + let trans = event.target.transaction; + + for (let autoincrement of [true, false]) { + for (let keypath of [false, true, "missing", "invalid"]) { + for (let method of ["put", "add"]) { + for (let explicit of [true, false, undefined, "invalid"]) { + for (let existing of [true, false]) { + let speccedNoKey = (keypath == false || keypath == "missing") && + !explicit; + + // We can't do 'existing' checks if we use autogenerated key + if (speccedNoKey && autoincrement && existing) { + continue; + } + + // Create store + if (db.objectStoreNames.contains("mystore")) + db.deleteObjectStore("mystore"); + let store = db.createObjectStore("mystore", + { autoIncrement: autoincrement, + keyPath: (keypath ? "id" : null) }); + + test = " for test " + JSON.stringify({ autoincrement: autoincrement, + keypath: keypath, + method: method, + explicit: explicit === undefined ? "undefined" : explicit, + existing: existing }); + + // Insert "existing" data if needed + if (existing) { + if (keypath) + store.add({ existing: "data", id: 5 }).onsuccess = grabEventAndContinueHandler; + else + store.add({ existing: "data" }, 5).onsuccess = grabEventAndContinueHandler; + + let e = yield undefined; + is(e.type, "success", "success inserting existing" + test); + is(e.target.result, 5, "inserted correct key" + test); + } + + // Set up value to be inserted + let value = { theObj: true }; + if (keypath === true) { + value.id = 5; + } + else if (keypath === "invalid") { + value.id = /x/; + } + + // Which arguments are passed to function + args = [value]; + if (explicit === true) { + args.push(5); + } + else if (explicit === undefined) { + args.push(undefined); + } + else if (explicit === "invalid") { + args.push(/x/); + } + + let expected = expectedResult(method, keypath, explicit, autoincrement, existing); + + let valueJSON = JSON.stringify(value); + + ok(true, "making call" + test); + + // Make function call for throwing functions + if (expected === "throw") { + try { + store[method].apply(store, args); + ok(false, "should have thrown" + test); + } + catch (ex) { + ok(true, "did throw" + test); + ok(ex instanceof DOMException, "Got a DOMException" + test); + is(ex.name, "DataError", "expect a DataError" + test); + is(ex.code, 0, "expect zero" + test); + is(JSON.stringify(value), valueJSON, "call didn't modify value" + test); + } + continue; + } + + // Make non-throwing function call + let req = store[method].apply(store, args); + is(JSON.stringify(value), valueJSON, "call didn't modify value" + test); + + req.onsuccess = req.onerror = grabEventAndContinueHandler; + let e = yield undefined; + + // Figure out what key we used + let key = 5; + if (autoincrement && speccedNoKey) { + key = 1; + } + + // Adjust value if expected + if (autoincrement && keypath && speccedNoKey) { + value.id = key; + } + + // Check result + if (expected === "error") { + is(e.type, "error", "write should fail" + test); + e.preventDefault(); + e.stopPropagation(); + continue; + } + + is(e.type, "success", "write should succeed" + test); + is(e.target.result, key, "write should return correct key" + test); + + store.get(key).onsuccess = grabEventAndContinueHandler; + e = yield undefined; + is(e.type, "success", "read back should succeed" + test); + is(JSON.stringify(e.target.result), + JSON.stringify(value), + "read back should return correct value" + test); + } + } + } + } + } + + + function expectedResult(method, keypath, explicit, autoincrement, existing) { + if (keypath && explicit) + return "throw"; + if (!keypath && !explicit && !autoincrement) + return "throw"; + if (keypath == "invalid") + return "throw"; + if (keypath == "missing" && !autoincrement) + return "throw"; + if (explicit == "invalid") + return "throw"; + + if (method == "add" && existing) + return "error"; + + return "success"; + } + + openRequest.onsuccess = grabEventAndContinueHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_add_twice_failure.js b/dom/indexedDB/test/unit/test_add_twice_failure.js new file mode 100644 index 000000000..80a4e5c55 --- /dev/null +++ b/dom/indexedDB/test/unit/test_add_twice_failure.js @@ -0,0 +1,43 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = request.result; + + ok(event.target === request, "Good event target"); + + let objectStore = db.createObjectStore("foo", { keyPath: null }); + let key = 10; + + request = objectStore.add({}, key); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(request.result, key, "Correct key"); + + request = objectStore.add({}, key); + request.addEventListener("error", new ExpectError("ConstraintError", true)); + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // Wait for success. + yield undefined; + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_advance.js b/dom/indexedDB/test/unit/test_advance.js new file mode 100644 index 000000000..3187a8f5e --- /dev/null +++ b/dom/indexedDB/test/unit/test_advance.js @@ -0,0 +1,192 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const dataCount = 30; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; + + let objectStore = db.createObjectStore("", { keyPath: "key" }); + objectStore.createIndex("", "index"); + + for (let i = 0; i < dataCount; i++) { + objectStore.add({ key: i, index: i }); + } + yield undefined; + + function getObjectStore() { + return db.transaction("").objectStore(""); + } + + function getIndex() { + return db.transaction("").objectStore("").index(""); + } + + let count = 0; + + getObjectStore().openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, dataCount, "Saw all data"); + + count = 0; + + getObjectStore().openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.primaryKey, count, "Got correct object"); + if (count) { + count++; + cursor.continue(); + } + else { + count = 10; + cursor.advance(10); + } + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, dataCount, "Saw all data"); + + count = 0; + + getIndex().openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.primaryKey, count, "Got correct object"); + if (count) { + count++; + cursor.continue(); + } + else { + count = 10; + cursor.advance(10); + } + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, dataCount, "Saw all data"); + + count = 0; + + getIndex().openKeyCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.primaryKey, count, "Got correct object"); + if (count) { + count++; + cursor.continue(); + } + else { + count = 10; + cursor.advance(10); + } + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, dataCount, "Saw all data"); + + count = 0; + + getObjectStore().openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.primaryKey, count, "Got correct object"); + if (count == 0) { + cursor.advance(dataCount + 1); + } + else { + ok(false, "Should never get here!"); + cursor.continue(); + } + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, 0, "Saw all data"); + + count = dataCount - 1; + + getObjectStore().openCursor(null, "prev").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.primaryKey, count, "Got correct object"); + count--; + if (count == dataCount - 2) { + cursor.advance(10); + count -= 9; + } + else { + cursor.continue(); + } + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, -1, "Saw all data"); + + count = dataCount - 1; + + getObjectStore().openCursor(null, "prev").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.primaryKey, count, "Got correct object"); + if (count == dataCount - 1) { + cursor.advance(dataCount + 1); + } + else { + ok(false, "Should never get here!"); + cursor.continue(); + } + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(count, dataCount - 1, "Saw all data"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_autoIncrement.js b/dom/indexedDB/test/unit/test_autoIncrement.js new file mode 100644 index 000000000..f2ea09822 --- /dev/null +++ b/dom/indexedDB/test/unit/test_autoIncrement.js @@ -0,0 +1,400 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need to implement a gc() function for worker tests"; + +if (!this.window) { + this.runTest = function() { + todo(false, "Test disabled in xpcshell test suite for now"); + finishTest(); + } +} + +var testGenerator = testSteps(); + +function genCheck(key, value, test, options) { + return function(event) { + is(JSON.stringify(event.target.result), JSON.stringify(key), + "correct returned key in " + test); + if (options && options.store) { + is(event.target.source, options.store, "correct store in " + test); + } + if (options && options.trans) { + is(event.target.transaction, options.trans, "correct transaction in " + test); + } + + event.target.source.get(key).onsuccess = function(event) { + is(JSON.stringify(event.target.result), JSON.stringify(value), + "correct stored value in " + test); + continueToNextStepSync(); + } + } +} + +function testSteps() +{ + const dbname = this.window ? window.location.pathname : "Splendid Test"; + const RW = "readwrite"; + let c1 = 1; + let c2 = 1; + + let openRequest = indexedDB.open(dbname, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + let trans = event.target.transaction; + + // Create test stores + let store1 = db.createObjectStore("store1", { autoIncrement: true }); + let store2 = db.createObjectStore("store2", { autoIncrement: true, keyPath: "id" }); + let store3 = db.createObjectStore("store3", { autoIncrement: false }); + is(store1.autoIncrement, true, "store1 .autoIncrement"); + is(store2.autoIncrement, true, "store2 .autoIncrement"); + is(store3.autoIncrement, false, "store3 .autoIncrement"); + + store1.createIndex("unique1", "unique", { unique: true }); + store2.createIndex("unique1", "unique", { unique: true }); + + // Test simple inserts + let test = " for test simple insert" + store1.add({ foo: "value1" }).onsuccess = + genCheck(c1++, { foo: "value1" }, "first" + test); + store1.add({ foo: "value2" }).onsuccess = + genCheck(c1++, { foo: "value2" }, "second" + test); + + yield undefined; + yield undefined; + + store2.put({ bar: "value1" }).onsuccess = + genCheck(c2, { bar: "value1", id: c2 }, "first in store2" + test, + { store: store2 }); + c2++; + store1.put({ foo: "value3" }).onsuccess = + genCheck(c1++, { foo: "value3" }, "third" + test, + { store: store1 }); + + yield undefined; + yield undefined; + + store2.get(IDBKeyRange.lowerBound(c2)).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + is(event.target.result, undefined, "no such value" + test); + + // Close version_change transaction + openRequest.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target, openRequest, "succeeded to open" + test); + is(event.type, "success", "succeeded to open" + test); + + // Test inserting explicit keys + test = " for test explicit keys"; + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 1 }, 100).onsuccess = + genCheck(100, { explicit: 1 }, "first" + test); + c1 = 101; + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 2 }).onsuccess = + genCheck(c1++, { explicit: 2 }, "second" + test); + yield undefined; yield undefined; + + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 3 }, 200).onsuccess = + genCheck(200, { explicit: 3 }, "third" + test); + c1 = 201; + trans.objectStore("store1").add({ explicit: 4 }).onsuccess = + genCheck(c1++, { explicit: 4 }, "fourth" + test); + yield undefined; yield undefined; + + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 5 }, 150).onsuccess = + genCheck(150, { explicit: 5 }, "fifth" + test); + yield undefined; + trans.objectStore("store1").add({ explicit: 6 }).onsuccess = + genCheck(c1++, { explicit: 6 }, "sixth" + test); + yield undefined; + + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 7 }, "key").onsuccess = + genCheck("key", { explicit: 7 }, "seventh" + test); + yield undefined; + trans.objectStore("store1").add({ explicit: 8 }).onsuccess = + genCheck(c1++, { explicit: 8 }, "eighth" + test); + yield undefined; + + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 7 }, [100000]).onsuccess = + genCheck([100000], { explicit: 7 }, "seventh" + test); + yield undefined; + trans.objectStore("store1").add({ explicit: 8 }).onsuccess = + genCheck(c1++, { explicit: 8 }, "eighth" + test); + yield undefined; + + trans = db.transaction("store1", RW); + trans.objectStore("store1").add({ explicit: 9 }, -100000).onsuccess = + genCheck(-100000, { explicit: 9 }, "ninth" + test); + yield undefined; + trans.objectStore("store1").add({ explicit: 10 }).onsuccess = + genCheck(c1++, { explicit: 10 }, "tenth" + test); + yield undefined; + + + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit2: 1, id: 300 }).onsuccess = + genCheck(300, { explicit2: 1, id: 300 }, "first store2" + test); + c2 = 301; + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit2: 2 }).onsuccess = + genCheck(c2, { explicit2: 2, id: c2 }, "second store2" + test); + c2++; + yield undefined; yield undefined; + + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit2: 3, id: 400 }).onsuccess = + genCheck(400, { explicit2: 3, id: 400 }, "third store2" + test); + c2 = 401; + trans.objectStore("store2").add({ explicit2: 4 }).onsuccess = + genCheck(c2, { explicit2: 4, id: c2 }, "fourth store2" + test); + c2++; + yield undefined; yield undefined; + + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit: 5, id: 150 }).onsuccess = + genCheck(150, { explicit: 5, id: 150 }, "fifth store2" + test); + yield undefined; + trans.objectStore("store2").add({ explicit: 6 }).onsuccess = + genCheck(c2, { explicit: 6, id: c2 }, "sixth store2" + test); + c2++; + yield undefined; + + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit: 7, id: "key" }).onsuccess = + genCheck("key", { explicit: 7, id: "key" }, "seventh store2" + test); + yield undefined; + trans.objectStore("store2").add({ explicit: 8 }).onsuccess = + genCheck(c2, { explicit: 8, id: c2 }, "eighth store2" + test); + c2++; + yield undefined; + + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit: 7, id: [100000] }).onsuccess = + genCheck([100000], { explicit: 7, id: [100000] }, "seventh store2" + test); + yield undefined; + trans.objectStore("store2").add({ explicit: 8 }).onsuccess = + genCheck(c2, { explicit: 8, id: c2 }, "eighth store2" + test); + c2++; + yield undefined; + + trans = db.transaction("store2", RW); + trans.objectStore("store2").add({ explicit: 9, id: -100000 }).onsuccess = + genCheck(-100000, { explicit: 9, id: -100000 }, "ninth store2" + test); + yield undefined; + trans.objectStore("store2").add({ explicit: 10 }).onsuccess = + genCheck(c2, { explicit: 10, id: c2 }, "tenth store2" + test); + c2++; + yield undefined; + + + // Test separate transactions doesn't generate overlapping numbers + test = " for test non-overlapping counts"; + trans = db.transaction("store1", RW); + trans2 = db.transaction("store1", RW); + trans2.objectStore("store1").put({ over: 2 }).onsuccess = + genCheck(c1 + 1, { over: 2 }, "first" + test, + { trans: trans2 }); + trans.objectStore("store1").put({ over: 1 }).onsuccess = + genCheck(c1, { over: 1 }, "second" + test, + { trans: trans }); + c1 += 2; + yield undefined; yield undefined; + + trans = db.transaction("store2", RW); + trans2 = db.transaction("store2", RW); + trans2.objectStore("store2").put({ over: 2 }).onsuccess = + genCheck(c2 + 1, { over: 2, id: c2 + 1 }, "third" + test, + { trans: trans2 }); + trans.objectStore("store2").put({ over: 1 }).onsuccess = + genCheck(c2, { over: 1, id: c2 }, "fourth" + test, + { trans: trans }); + c2 += 2; + yield undefined; yield undefined; + + // Test that error inserts doesn't increase generator + test = " for test error inserts"; + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ unique: 1 }, -1); + trans.objectStore("store2").add({ unique: 1, id: "unique" }); + + trans.objectStore("store1").add({ error: 1, unique: 1 }). + addEventListener("error", new ExpectError("ConstraintError", true)); + trans.objectStore("store1").add({ error: 2 }).onsuccess = + genCheck(c1++, { error: 2 }, "first" + test); + yield undefined; yield undefined; + + trans.objectStore("store2").add({ error: 3, unique: 1 }). + addEventListener("error", new ExpectError("ConstraintError", true)); + trans.objectStore("store2").add({ error: 4 }).onsuccess = + genCheck(c2, { error: 4, id: c2 }, "second" + test); + c2++; + yield undefined; yield undefined; + + trans.objectStore("store1").add({ error: 5, unique: 1 }, 100000). + addEventListener("error", new ExpectError("ConstraintError", true)); + trans.objectStore("store1").add({ error: 6 }).onsuccess = + genCheck(c1++, { error: 6 }, "third" + test); + yield undefined; yield undefined; + + trans.objectStore("store2").add({ error: 7, unique: 1, id: 100000 }). + addEventListener("error", new ExpectError("ConstraintError", true)); + trans.objectStore("store2").add({ error: 8 }).onsuccess = + genCheck(c2, { error: 8, id: c2 }, "fourth" + test); + c2++; + yield undefined; yield undefined; + + // Test that aborts doesn't increase generator + test = " for test aborted transaction"; + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ abort: 1 }).onsuccess = + genCheck(c1, { abort: 1 }, "first" + test); + trans.objectStore("store2").put({ abort: 2 }).onsuccess = + genCheck(c2, { abort: 2, id: c2 }, "second" + test); + yield undefined; yield undefined; + + trans.objectStore("store1").add({ abort: 3 }, 500).onsuccess = + genCheck(500, { abort: 3 }, "third" + test); + trans.objectStore("store2").put({ abort: 4, id: 600 }).onsuccess = + genCheck(600, { abort: 4, id: 600 }, "fourth" + test); + yield undefined; yield undefined; + + trans.objectStore("store1").add({ abort: 5 }).onsuccess = + genCheck(501, { abort: 5 }, "fifth" + test); + trans.objectStore("store2").put({ abort: 6 }).onsuccess = + genCheck(601, { abort: 6, id: 601 }, "sixth" + test); + yield undefined; yield undefined; + + trans.abort(); + trans.onabort = grabEventAndContinueHandler; + event = yield + is(event.type, "abort", "transaction aborted"); + is(event.target, trans, "correct transaction aborted"); + + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ abort: 1 }).onsuccess = + genCheck(c1++, { abort: 1 }, "re-first" + test); + trans.objectStore("store2").put({ abort: 2 }).onsuccess = + genCheck(c2, { abort: 2, id: c2 }, "re-second" + test); + c2++; + yield undefined; yield undefined; + + // Test that delete doesn't decrease generator + test = " for test delete items" + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ delete: 1 }).onsuccess = + genCheck(c1++, { delete: 1 }, "first" + test); + trans.objectStore("store2").put({ delete: 2 }).onsuccess = + genCheck(c2, { delete: 2, id: c2 }, "second" + test); + c2++; + yield undefined; yield undefined; + + trans.objectStore("store1").delete(c1 - 1).onsuccess = + grabEventAndContinueHandler; + trans.objectStore("store2").delete(c2 - 1).onsuccess = + grabEventAndContinueHandler; + yield undefined; yield undefined; + + trans.objectStore("store1").add({ delete: 3 }).onsuccess = + genCheck(c1++, { delete: 3 }, "first" + test); + trans.objectStore("store2").put({ delete: 4 }).onsuccess = + genCheck(c2, { delete: 4, id: c2 }, "second" + test); + c2++; + yield undefined; yield undefined; + + trans.objectStore("store1").delete(c1 - 1).onsuccess = + grabEventAndContinueHandler; + trans.objectStore("store2").delete(c2 - 1).onsuccess = + grabEventAndContinueHandler; + yield undefined; yield undefined; + + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ delete: 5 }).onsuccess = + genCheck(c1++, { delete: 5 }, "first" + test); + trans.objectStore("store2").put({ delete: 6 }).onsuccess = + genCheck(c2, { delete: 6, id: c2 }, "second" + test); + c2++; + yield undefined; yield undefined; + + // Test that clears doesn't decrease generator + test = " for test clear stores"; + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ clear: 1 }).onsuccess = + genCheck(c1++, { clear: 1 }, "first" + test); + trans.objectStore("store2").put({ clear: 2 }).onsuccess = + genCheck(c2, { clear: 2, id: c2 }, "second" + test); + c2++; + yield undefined; yield undefined; + + trans.objectStore("store1").clear().onsuccess = + grabEventAndContinueHandler; + trans.objectStore("store2").clear().onsuccess = + grabEventAndContinueHandler; + yield undefined; yield undefined; + + trans.objectStore("store1").add({ clear: 3 }).onsuccess = + genCheck(c1++, { clear: 3 }, "third" + test); + trans.objectStore("store2").put({ clear: 4 }).onsuccess = + genCheck(c2, { clear: 4, id: c2 }, "forth" + test); + c2++; + yield undefined; yield undefined; + + trans.objectStore("store1").clear().onsuccess = + grabEventAndContinueHandler; + trans.objectStore("store2").clear().onsuccess = + grabEventAndContinueHandler; + yield undefined; yield undefined; + + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").add({ clear: 5 }).onsuccess = + genCheck(c1++, { clear: 5 }, "fifth" + test); + trans.objectStore("store2").put({ clear: 6 }).onsuccess = + genCheck(c2, { clear: 6, id: c2 }, "sixth" + test); + c2++; + yield undefined; yield undefined; + + + // Test that close/reopen doesn't decrease generator + test = " for test clear stores"; + trans = db.transaction(["store1", "store2"], RW); + trans.objectStore("store1").clear().onsuccess = + grabEventAndContinueHandler; + trans.objectStore("store2").clear().onsuccess = + grabEventAndContinueHandler; + yield undefined; yield undefined; + db.close(); + + gc(); + + openRequest = indexedDB.open(dbname, 2); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + db = event.target.result; + trans = event.target.transaction; + + trans.objectStore("store1").add({ reopen: 1 }).onsuccess = + genCheck(c1++, { reopen: 1 }, "first" + test); + trans.objectStore("store2").put({ reopen: 2 }).onsuccess = + genCheck(c2, { reopen: 2, id: c2 }, "second" + test); + c2++; + yield undefined; yield undefined; + + openRequest.onsuccess = grabEventAndContinueHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_autoIncrement_indexes.js b/dom/indexedDB/test/unit/test_autoIncrement_indexes.js new file mode 100644 index 000000000..ce2d5d552 --- /dev/null +++ b/dom/indexedDB/test/unit/test_autoIncrement_indexes.js @@ -0,0 +1,56 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = request.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore("foo", { keyPath: "id", + autoIncrement: true }); + objectStore.createIndex("first","first"); + objectStore.createIndex("second","second"); + objectStore.createIndex("third","third"); + + let data = { first: "foo", second: "foo", third: "foo" }; + + objectStore.add(data).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, "Added entry"); + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + objectStore = db.transaction("foo").objectStore("foo"); + let first = objectStore.index("first"); + let second = objectStore.index("second"); + let third = objectStore.index("third"); + + first.get("foo").onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is (event.target.result.id, 1, "Entry in first"); + + second.get("foo").onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is (event.target.result.id, 1, "Entry in second"); + + third.get("foo").onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is (event.target.result.id, 1, "Entry in third"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_blob_file_backed.js b/dom/indexedDB/test/unit/test_blob_file_backed.js new file mode 100644 index 000000000..664c9e2c9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_blob_file_backed.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "This test uses SpecialPowers"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const fileData = "abcdefghijklmnopqrstuvwxyz"; + const fileType = "text/plain"; + + const databaseName = + ("window" in this) ? window.location.pathname : "Test"; + const objectStoreName = "foo"; + const objectStoreKey = "10"; + + info("Creating temp file"); + + SpecialPowers.createFiles([{data:fileData, options:{type:fileType}}], function (files) { + testGenerator.next(files[0]); + }); + + let file = yield undefined; + + ok(file instanceof File, "Got a File object"); + is(file.size, fileData.length, "Correct size"); + is(file.type, fileType, "Correct type"); + + let fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(file); + + let event = yield undefined; + + is(fileReader.result, fileData, "Correct data"); + + let request = indexedDB.open(databaseName, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName); + objectStore.put(file, objectStoreKey); + + event = yield undefined; + + db = event.target.result; + + file = null; + + objectStore = db.transaction(objectStoreName).objectStore(objectStoreName); + objectStore.get(objectStoreKey).onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + file = event.target.result; + + ok(file instanceof File, "Got a File object"); + is(file.size, fileData.length, "Correct size"); + is(file.type, fileType, "Correct type"); + + fileReader = new FileReader(); + fileReader.onload = grabEventAndContinueHandler; + fileReader.readAsText(file); + + event = yield undefined; + + is(fileReader.result, fileData, "Correct data"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_blocked_order.js b/dom/indexedDB/test/unit/test_blocked_order.js new file mode 100644 index 000000000..1c70853f4 --- /dev/null +++ b/dom/indexedDB/test/unit/test_blocked_order.js @@ -0,0 +1,179 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const databaseName = + ("window" in this) ? window.location.pathname : "Test"; + const databaseCount = 10; + + // Test 1: Make sure basic versionchange events work and that they don't + // trigger blocked events. + info("Opening " + databaseCount + " databases with version 1"); + + let databases = []; + + for (let i = 0; i < databaseCount; i++) { + let thisIndex = i; + + info("Opening database " + thisIndex); + + let request = indexedDB.open(databaseName, 1); + request.onerror = errorHandler; + request.onblocked = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "success", "Got success event"); + + let db = request.result; + is(db.version, 1, "Got version 1"); + + db.onversionchange = function(event) { + info("Closing database " + thisIndex); + db.close(); + + databases.splice(databases.indexOf(db), 1); + }; + + databases.push(db); + } + + is(databases.length, databaseCount, "Created all databases with version 1"); + + info("Opening database with version 2"); + + let request = indexedDB.open(databaseName, 2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + request.onblocked = function(event) { + ok(false, "Should not receive a blocked event"); + }; + + let event = yield undefined; + + is(event.type, "success", "Got success event"); + is(databases.length, 0, "All databases with version 1 were closed"); + + let db = request.result; + is(db.version, 2, "Got version 2"); + + info("Deleting database with version 2"); + db.close(); + + request = indexedDB.deleteDatabase(databaseName); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Got success event"); + + // Test 2: Make sure blocked events aren't delivered until all versionchange + // events have been delivered. + info("Opening " + databaseCount + " databases with version 1"); + + for (let i = 0; i < databaseCount; i++) { + let thisIndex = i; + + info("Opening database " + thisIndex); + + let request = indexedDB.open(databaseName, 1); + request.onerror = errorHandler; + request.onblocked = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "success", "Got success event"); + + let db = request.result; + is(db.version, 1, "Got version 1"); + + db.onversionchange = function(event) { + if (thisIndex == (databaseCount - 1)) { + info("Closing all databases with version 1"); + + for (let j = 0; j < databases.length; j++) { + databases[j].close(); + } + + databases = []; + info("Done closing all databases with version 1"); + } else { + info("Not closing database " + thisIndex); + } + }; + + databases.push(db); + } + + is(databases.length, databaseCount, "Created all databases with version 1"); + + info("Opening database with version 2"); + + request = indexedDB.open(databaseName, 2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + request.onblocked = function(event) { + ok(false, "Should not receive a blocked event"); + }; + + event = yield undefined; + + is(event.type, "success", "Got success event"); + is(databases.length, 0, "All databases with version 1 were closed"); + + db = request.result; + is(db.version, 2, "Got version 2"); + + info("Deleting database with version 2"); + db.close(); + + request = indexedDB.deleteDatabase(databaseName); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Got success event"); + + // Test 3: A blocked database left in that state should not hang shutdown. + info("Opening 1 database with version 1"); + + request = indexedDB.open(databaseName, 1); + request.onerror = errorHandler; + request.onblocked = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Got success event"); + + db = request.result; + is(db.version, 1, "Got version 1"); + + info("Opening database with version 2"); + + request = indexedDB.open(databaseName, 2); + request.onerror = function(e) { + e.preventDefault(); + }; + request.onsuccess = errorHandler; + + request.onblocked = grabEventAndContinueHandler; + + event = yield undefined; + ok(true, "Got blocked"); + // Just allow this to remain blocked ... + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_bug1056939.js b/dom/indexedDB/test/unit/test_bug1056939.js new file mode 100644 index 000000000..49bccd9ff --- /dev/null +++ b/dom/indexedDB/test/unit/test_bug1056939.js @@ -0,0 +1,73 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbName1 = "upgrade_test"; + const dbName2 = "testing.foobar"; + const dbName3 = "xxxxxxx.xxxxxx"; + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("bug1056939_profile"); + + let request = indexedDB.open(dbName1, 1); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + + request = indexedDB.open(dbName2, 1); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + request = indexedDB.open(dbName3, 1); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + request = indexedDB.open(dbName3, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + request = indexedDB.open(dbName3, 1); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_cleanup_transaction.js b/dom/indexedDB/test/unit/test_cleanup_transaction.js new file mode 100644 index 000000000..070e9015e --- /dev/null +++ b/dom/indexedDB/test/unit/test_cleanup_transaction.js @@ -0,0 +1,155 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need a way to set temporary prefs from a worker"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const spec = "http://foo.com"; + const name = + this.window ? window.location.pathname : "test_quotaExceeded_recovery"; + const objectStoreName = "foo"; + + // We want 32 MB database, but there's the group limit so we need to + // multiply by 5. + const tempStorageLimitKB = 32 * 1024 * 5; + + // Store in 1 MB chunks. + const dataSize = 1024 * 1024; + + for (let blobs of [false, true]) { + setTemporaryStorageLimit(tempStorageLimitKB); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + info("Opening database"); + + let request = indexedDB.openForPrincipal(getPrincipal(spec), name); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler;; + request.onsuccess = unexpectedSuccessHandler; + + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + ok(true, "Adding data until quota is reached"); + + let obj = { + name: "foo" + } + + if (!blobs) { + obj.data = getRandomView(dataSize); + } + + let i = 1; + let j = 1; + while (true) { + if (blobs) { + obj.data = getBlob(getView(dataSize)); + } + + let trans = db.transaction(objectStoreName, "readwrite"); + request = trans.objectStore(objectStoreName).add(obj, i); + request.onerror = function(event) + { + event.stopPropagation(); + } + + trans.oncomplete = function(event) { + i++; + j++; + testGenerator.send(true); + } + trans.onabort = function(event) { + is(trans.error.name, "QuotaExceededError", "Reached quota limit"); + testGenerator.send(false); + } + + let completeFired = yield undefined; + if (completeFired) { + ok(true, "Got complete event"); + } else { + ok(true, "Got abort event"); + + if (j == 1) { + // Plain cleanup transaction (just vacuuming and checkpointing) + // couldn't shrink database any further. + break; + } + + j = 1; + + trans = db.transaction(objectStoreName, "cleanup"); + trans.onabort = unexpectedSuccessHandler;; + trans.oncomplete = grabEventAndContinueHandler; + + yield undefined; + } + } + + info("Reopening database"); + + db.close(); + + request = indexedDB.openForPrincipal(getPrincipal(spec), name); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + yield undefined; + + db = request.result; + db.onerror = errorHandler; + + info("Deleting some data") + + let trans = db.transaction(objectStoreName, "cleanup"); + trans.objectStore(objectStoreName).delete(1); + + trans.onabort = unexpectedSuccessHandler;; + trans.oncomplete = grabEventAndContinueHandler; + + yield undefined; + + info("Adding data again") + + trans = db.transaction(objectStoreName, "readwrite"); + trans.objectStore(objectStoreName).add(obj, 1); + + trans.onabort = unexpectedSuccessHandler; + trans.oncomplete = grabEventAndContinueHandler; + + yield undefined; + + info("Deleting database"); + + db.close(); + + request = indexedDB.deleteForPrincipal(getPrincipal(spec), name); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + yield undefined; + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_clear.js b/dom/indexedDB/test/unit/test_clear.js new file mode 100644 index 000000000..acce5c261 --- /dev/null +++ b/dom/indexedDB/test/unit/test_clear.js @@ -0,0 +1,97 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const entryCount = 1000; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = request.result; + + event.target.onsuccess = continueToNextStep; + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + let firstKey; + for (let i = 0; i < entryCount; i++) { + request = objectStore.add({}); + request.onerror = errorHandler; + if (!i) { + request.onsuccess = function(event) { + firstKey = event.target.result; + }; + } + } + yield undefined; + + isnot(firstKey, undefined, "got first key"); + + let seenEntryCount = 0; + + request = db.transaction("foo").objectStore("foo").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + seenEntryCount++; + cursor.continue(); + } + else { + continueToNextStep(); + } + } + yield undefined; + + is(seenEntryCount, entryCount, "Correct entry count"); + + try { + db.transaction("foo").objectStore("foo").clear(); + ok(false, "clear should throw on READ_ONLY transactions"); + } + catch (e) { + ok(true, "clear should throw on READ_ONLY transactions"); + } + + request = db.transaction("foo", "readwriteflush") + .objectStore("foo") + .clear(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.result === undefined, "Correct event.target.result"); + ok(request.result === undefined, "Correct request.result"); + ok(request === event.target, "Correct event.target"); + + request = db.transaction("foo").objectStore("foo").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function(event) { + let cursor = request.result; + if (cursor) { + ok(false, "Shouldn't have any entries"); + } + continueToNextStep(); + } + yield undefined; + + request = db.transaction("foo", "readwrite") + .objectStore("foo") + .add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + isnot(event.target.result, firstKey, "Got a different key"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_complex_keyPaths.js b/dom/indexedDB/test/unit/test_complex_keyPaths.js new file mode 100644 index 000000000..24375813d --- /dev/null +++ b/dom/indexedDB/test/unit/test_complex_keyPaths.js @@ -0,0 +1,266 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + // Test object stores + + const name = "test_complex_keyPaths"; + const keyPaths = [ + { keyPath: "id", value: { id: 5 }, key: 5 }, + { keyPath: "id", value: { id: "14", iid: 12 }, key: "14" }, + { keyPath: "id", value: { iid: "14", id: 12 }, key: 12 }, + { keyPath: "id", value: {} }, + { keyPath: "id", value: { id: {} } }, + { keyPath: "id", value: { id: /x/ } }, + { keyPath: "id", value: 2 }, + { keyPath: "id", value: undefined }, + { keyPath: "foo.id", value: { foo: { id: 7 } }, key: 7 }, + { keyPath: "foo.id", value: { id: 7, foo: { id: "asdf" } }, key: "asdf" }, + { keyPath: "foo.id", value: { foo: { id: undefined } } }, + { keyPath: "foo.id", value: { foo: 47 } }, + { keyPath: "foo.id", value: {} }, + { keyPath: "", value: "foopy", key: "foopy" }, + { keyPath: "", value: 2, key: 2 }, + { keyPath: "", value: undefined }, + { keyPath: "", value: { id: 12 } }, + { keyPath: "", value: /x/ }, + { keyPath: "foo.bar", value: { baz: 1, foo: { baz2: 2, bar: "xo" } }, key: "xo" }, + { keyPath: "foo.bar.baz", value: { foo: { bar: { bazz: 16, baz: 17 } } }, key: 17 }, + { keyPath: "foo..id", exception: true }, + { keyPath: "foo.", exception: true }, + { keyPath: "fo o", exception: true }, + { keyPath: "foo ", exception: true }, + { keyPath: "foo[bar]",exception: true }, + { keyPath: "foo[1]", exception: true }, + { keyPath: "$('id').stuff", exception: true }, + { keyPath: "foo.2.bar", exception: true }, + { keyPath: "foo. .bar", exception: true }, + { keyPath: ".bar", exception: true }, + { keyPath: [], exception: true }, + + { keyPath: ["foo", "bar"], value: { foo: 1, bar: 2 }, key: [1, 2] }, + { keyPath: ["foo"], value: { foo: 1, bar: 2 }, key: [1] }, + { keyPath: ["foo", "bar", "bar"], value: { foo: 1, bar: "x" }, key: [1, "x", "x"] }, + { keyPath: ["x", "y"], value: { x: [], y: "x" }, key: [[], "x"] }, + { keyPath: ["x", "y"], value: { x: [[1]], y: "x" }, key: [[[1]], "x"] }, + { keyPath: ["x", "y"], value: { x: [[1]], y: new Date(1) }, key: [[[1]], new Date(1)] }, + { keyPath: ["x", "y"], value: { x: [[1]], y: [new Date(3)] }, key: [[[1]], [new Date(3)]] }, + { keyPath: ["x", "y.bar"], value: { x: "hi", y: { bar: "x"} }, key: ["hi", "x"] }, + { keyPath: ["x.y", "y.bar"], value: { x: { y: "hello" }, y: { bar: "nurse"} }, key: ["hello", "nurse"] }, + { keyPath: ["", ""], value: 5, key: [5, 5] }, + { keyPath: ["x", "y"], value: { x: 1 } }, + { keyPath: ["x", "y"], value: { y: 1 } }, + { keyPath: ["x", "y"], value: { x: 1, y: undefined } }, + { keyPath: ["x", "y"], value: { x: null, y: 1 } }, + { keyPath: ["x", "y.bar"], value: { x: null, y: { bar: "x"} } }, + { keyPath: ["x", "y"], value: { x: 1, y: false } }, + { keyPath: ["x", "y", "z"], value: { x: 1, y: false, z: "a" } }, + { keyPath: [".x", "y", "z"], exception: true }, + { keyPath: ["x", "y ", "z"], exception: true }, + ]; + + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + + let stores = {}; + + // Test creating object stores and inserting data + for (let i = 0; i < keyPaths.length; i++) { + let info = keyPaths[i]; + + let test = " for objectStore test " + JSON.stringify(info); + let indexName = JSON.stringify(info.keyPath); + if (!stores[indexName]) { + try { + let objectStore = db.createObjectStore(indexName, { keyPath: info.keyPath }); + ok(!("exception" in info), "shouldn't throw" + test); + is(JSON.stringify(objectStore.keyPath), JSON.stringify(info.keyPath), + "correct keyPath property" + test); + ok(objectStore.keyPath === objectStore.keyPath, + "object identity should be preserved"); + stores[indexName] = objectStore; + } catch (e) { + ok("exception" in info, "should throw" + test); + is(e.name, "SyntaxError", "expect a SyntaxError" + test); + ok(e instanceof DOMException, "Got a DOM Exception" + test); + is(e.code, DOMException.SYNTAX_ERR, "expect a syntax error" + test); + continue; + } + } + + let store = stores[indexName]; + + try { + request = store.add(info.value); + ok("key" in info, "successfully created request to insert value" + test); + } catch (e) { + ok(!("key" in info), "threw when attempted to insert" + test); + ok(e instanceof DOMException, "Got a DOMException" + test); + is(e.name, "DataError", "expect a DataError" + test); + is(e.code, 0, "expect zero" + test); + continue; + } + + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + let e = yield undefined; + is(e.type, "success", "inserted successfully" + test); + is(e.target, request, "expected target" + test); + ok(compareKeys(request.result, info.key), "found correct key" + test); + is(indexedDB.cmp(request.result, info.key), 0, "returned key compares correctly" + test); + + store.get(info.key).onsuccess = grabEventAndContinueHandler; + e = yield undefined; + isnot(e.target.result, undefined, "Did find entry"); + + // Check that cursor.update work as expected + request = store.openCursor(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + e = yield undefined; + let cursor = e.target.result; + request = cursor.update(info.value); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + yield undefined; + ok(true, "Successfully updated cursor" + test); + + // Check that cursor.update throws as expected when key is changed + let newValue = cursor.value; + let destProp = Array.isArray(info.keyPath) ? info.keyPath[0] : info.keyPath; + if (destProp) { + eval("newValue." + destProp + " = 'newKeyValue'"); + } + else { + newValue = 'newKeyValue'; + } + let didThrow; + try { + cursor.update(newValue); + } + catch (ex) { + didThrow = ex; + } + ok(didThrow instanceof DOMException, "Got a DOMException" + test); + is(didThrow.name, "DataError", "expect a DataError" + test); + is(didThrow.code, 0, "expect zero" + test); + + // Clear object store to prepare for next test + store.clear().onsuccess = grabEventAndContinueHandler; + yield undefined; + } + + // Attempt to create indexes and insert data + let store = db.createObjectStore("indexStore"); + let indexes = {}; + for (let i = 0; i < keyPaths.length; i++) { + let info = keyPaths[i]; + let test = " for index test " + JSON.stringify(info); + let indexName = JSON.stringify(info.keyPath); + if (!indexes[indexName]) { + try { + let index = store.createIndex(indexName, info.keyPath); + ok(!("exception" in info), "shouldn't throw" + test); + is(JSON.stringify(index.keyPath), JSON.stringify(info.keyPath), + "index has correct keyPath property" + test); + ok(index.keyPath === index.keyPath, + "object identity should be preserved"); + indexes[indexName] = index; + } catch (e) { + ok("exception" in info, "should throw" + test); + is(e.name, "SyntaxError", "expect a SyntaxError" + test); + ok(e instanceof DOMException, "Got a DOM Exception" + test); + is(e.code, DOMException.SYNTAX_ERR, "expect a syntax error" + test); + continue; + } + } + + let index = indexes[indexName]; + + request = store.add(info.value, 1); + if ("key" in info) { + index.getKey(info.key).onsuccess = grabEventAndContinueHandler; + e = yield undefined; + is(e.target.result, 1, "found value when reading" + test); + } + else { + index.count().onsuccess = grabEventAndContinueHandler; + e = yield undefined; + is(e.target.result, 0, "should be empty" + test); + } + + store.clear().onsuccess = grabEventAndContinueHandler; + yield undefined; + } + + // Autoincrement and complex key paths + let aitests = [{ v: {}, k: 1, res: { foo: { id: 1 }} }, + { v: { value: "x" }, k: 2, res: { value: "x", foo: { id: 2 }} }, + { v: { value: "x", foo: {} }, k: 3, res: { value: "x", foo: { id: 3 }} }, + { v: { v: "x", foo: { x: "y" } }, k: 4, res: { v: "x", foo: { x: "y", id: 4 }} }, + { v: { value: 2, foo: { id: 10 }}, k: 10 }, + { v: { value: 2 }, k: 11, res: { value: 2, foo: { id: 11 }} }, + { v: true, }, + { v: { value: 2, foo: 12 }, }, + { v: { foo: { id: true }}, }, + { v: { foo: { x: 5, id: {} }}, }, + { v: undefined, }, + { v: { foo: undefined }, }, + { v: { foo: { id: undefined }}, }, + { v: null, }, + { v: { foo: null }, }, + { v: { foo: { id: null }}, }, + ]; + + store = db.createObjectStore("gen", { keyPath: "foo.id", autoIncrement: true }); + for (let i = 0; i < aitests.length; ++i) { + let info = aitests[i]; + let test = " for autoIncrement test " + JSON.stringify(info); + + let preValue = JSON.stringify(info.v); + if ("k" in info) { + store.add(info.v).onsuccess = grabEventAndContinueHandler; + is(JSON.stringify(info.v), preValue, "put didn't modify value" + test); + } + else { + try { + store.add(info.v); + ok(false, "should throw" + test); + } + catch(e) { + ok(true, "did throw" + test); + ok(e instanceof DOMException, "Got a DOMException" + test); + is(e.name, "DataError", "expect a DataError" + test); + is(e.code, 0, "expect zero" + test); + + is(JSON.stringify(info.v), preValue, "failing put didn't modify value" + test); + + continue; + } + } + + let e = yield undefined; + is(e.target.result, info.k, "got correct return key" + test); + + store.get(info.k).onsuccess = grabEventAndContinueHandler; + e = yield undefined; + is(JSON.stringify(e.target.result), JSON.stringify(info.res || info.v), + "expected value stored" + test); + } + + openRequest.onsuccess = grabEventAndContinueHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_count.js b/dom/indexedDB/test/unit/test_count.js new file mode 100644 index 000000000..32ba5e950 --- /dev/null +++ b/dom/indexedDB/test/unit/test_count.js @@ -0,0 +1,354 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7738", value: { name: "Mel", height: 66, weight: {} } }, + { key: "237-23-7739", value: { name: "Tom", height: 62, weight: 130 } } + ]; + + const indexData = { + name: "weight", + keyPath: "weight", + options: { unique: false } + }; + + const weightSort = [1, 0, 3, 7, 4, 2]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + objectStore.createIndex(indexData.name, indexData.keyPath, + indexData.options); + + for (let data of objectStoreData) { + objectStore.add(data.value, data.key); + } + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + objectStore = db.transaction(db.objectStoreNames) + .objectStore(objectStoreName); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length, + "Correct number of object store entries for all keys"); + + objectStore.count(null).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length, + "Correct number of object store entries for null key"); + + objectStore.count(objectStoreData[2].key).onsuccess = + grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, + "Correct number of object store entries for single existing key"); + + objectStore.count("foo").onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of object store entries for single non-existing key"); + + let keyRange = IDBKeyRange.only(objectStoreData[2].key); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, + "Correct number of object store entries for existing only keyRange"); + + keyRange = IDBKeyRange.only("foo"); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of object store entries for non-existing only keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[2].key); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length - 2, + "Correct number of object store entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[2].key, true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length - 3, + "Correct number of object store entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound("foo"); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of object store entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[2].key, false); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 3, + "Correct number of object store entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[2].key, true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 2, + "Correct number of object store entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound("foo", true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length, + "Correct number of object store entries for upperBound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[0].key, + objectStoreData[objectStoreData.length - 1].key); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length, + "Correct number of object store entries for bound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[0].key, + objectStoreData[objectStoreData.length - 1].key, + true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length - 1, + "Correct number of object store entries for bound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[0].key, + objectStoreData[objectStoreData.length - 1].key, + true, true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length - 2, + "Correct number of object store entries for bound keyRange"); + + keyRange = IDBKeyRange.bound("foo", "foopy", true, true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of object store entries for bound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[0].key, "foo", true, true); + objectStore.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreData.length - 1, + "Correct number of object store entries for bound keyRange"); + + let index = objectStore.index(indexData.name); + + index.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length, + "Correct number of index entries for no key"); + + index.count(objectStoreData[7].value.weight).onsuccess = + grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 2, + "Correct number of index entries for duplicate key"); + + index.count(objectStoreData[0].value.weight).onsuccess = + grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, + "Correct number of index entries for single key"); + + keyRange = IDBKeyRange.only(objectStoreData[0].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, + "Correct number of index entries for only existing keyRange"); + + keyRange = IDBKeyRange.only("foo"); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of index entries for only non-existing keyRange"); + + keyRange = IDBKeyRange.only(objectStoreData[7].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 2, + "Correct number of index entries for only duplicate keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[0]].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[1]].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length - 1, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[0]].value.weight - 1); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[0]].value.weight, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length - 1, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[weightSort.length - 1]].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[weightSort.length - 1]].value.weight, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.lowerBound(objectStoreData[weightSort[weightSort.length - 1]].value.weight + 1, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of index entries for lowerBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[weightSort[0]].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 1, + "Correct number of index entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[weightSort[0]].value.weight, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of index entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[weightSort[weightSort.length - 1]].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length, + "Correct number of index entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[weightSort[weightSort.length - 1]].value.weight, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length - 1, + "Correct number of index entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound(objectStoreData[weightSort[weightSort.length - 1]].value.weight, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length - 1, + "Correct number of index entries for upperBound keyRange"); + + keyRange = IDBKeyRange.upperBound("foo"); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length, + "Correct number of index entries for upperBound keyRange"); + + keyRange = IDBKeyRange.bound("foo", "foopy"); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, + "Correct number of index entries for bound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[weightSort[0]].value.weight, + objectStoreData[weightSort[weightSort.length - 1]].value.weight); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length, + "Correct number of index entries for bound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[weightSort[0]].value.weight, + objectStoreData[weightSort[weightSort.length - 1]].value.weight, + true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length - 1, + "Correct number of index entries for bound keyRange"); + + keyRange = IDBKeyRange.bound(objectStoreData[weightSort[0]].value.weight, + objectStoreData[weightSort[weightSort.length - 1]].value.weight, + true, true); + index.count(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, weightSort.length - 2, + "Correct number of index entries for bound keyRange"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_create_index.js b/dom/indexedDB/test/unit/test_create_index.js new file mode 100644 index 000000000..284cca2b3 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_index.js @@ -0,0 +1,121 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreInfo = [ + { name: "a", options: { keyPath: "id", autoIncrement: true } }, + { name: "b", options: { keyPath: "id", autoIncrement: false } }, + ]; + const indexInfo = [ + { name: "1", keyPath: "unique_value", options: { unique: true } }, + { name: "2", keyPath: "value", options: { unique: false } }, + { name: "3", keyPath: "value", options: { unique: false } }, + { name: "", keyPath: "value", options: { unique: false } }, + { name: null, keyPath: "value", options: { unique: false } }, + { name: undefined, keyPath: "value", options: { unique: false } }, + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + let objectStore = info.hasOwnProperty("options") ? + db.createObjectStore(info.name, info.options) : + db.createObjectStore(info.name); + + try { + request = objectStore.createIndex("Hola"); + ok(false, "createIndex with no keyPath should throw"); + } + catch(e) { + ok(true, "createIndex with no keyPath should throw"); + } + + let ex; + try { + objectStore.createIndex("Hola", ["foo"], { multiEntry: true }); + } + catch(e) { + ex = e; + } + ok(ex, "createIndex with array keyPath and multiEntry should throw"); + is(ex.name, "InvalidAccessError", "should throw right exception"); + ok(ex instanceof DOMException, "should throw right exception"); + is(ex.code, DOMException.INVALID_ACCESS_ERR, "should throw right exception"); + + try { + objectStore.createIndex("foo", "bar", 10); + ok(false, "createIndex with bad options should throw"); + } + catch(e) { + ok(true, "createIndex with bad options threw"); + } + + ok(objectStore.createIndex("foo", "bar", { foo: "" }), + "createIndex with unknown options should not throw"); + objectStore.deleteIndex("foo"); + + // Test index creation, and that it ends up in indexNames. + let objectStoreName = info.name; + for (let j = 0; j < indexInfo.length; j++) { + let info = indexInfo[j]; + let count = objectStore.indexNames.length; + let index = info.hasOwnProperty("options") ? + objectStore.createIndex(info.name, info.keyPath, + info.options) : + objectStore.createIndex(info.name, info.keyPath); + + let name = info.name; + if (name === null) { + name = "null"; + } + else if (name === undefined) { + name = "undefined"; + } + + is(index.name, name, "correct name"); + is(index.keyPath, info.keyPath, "correct keyPath"); + is(index.unique, info.options.unique, "correct uniqueness"); + + is(objectStore.indexNames.length, count + 1, + "indexNames grew in size"); + let found = false; + for (let k = 0; k < objectStore.indexNames.length; k++) { + if (objectStore.indexNames.item(k) == name) { + found = true; + break; + } + } + ok(found, "Name is on objectStore.indexNames"); + + ok(event.target.transaction, "event has a transaction"); + ok(event.target.transaction.db === db, + "transaction has the right db"); + is(event.target.transaction.mode, "versionchange", + "transaction has the correct mode"); + is(event.target.transaction.objectStoreNames.length, i + 1, + "transaction only has one object store"); + ok(event.target.transaction.objectStoreNames.contains(objectStoreName), + "transaction has the correct object store"); + } + } + + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + + event = yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_create_index_with_integer_keys.js b/dom/indexedDB/test/unit/test_create_index_with_integer_keys.js new file mode 100644 index 000000000..d14b50411 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_index_with_integer_keys.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const data = { id: new Date().getTime(), + num: parseInt(Math.random() * 1000) }; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; + + // Make object store, add data. + let objectStore = db.createObjectStore("foo", { keyPath: "id" }); + objectStore.add(data); + yield undefined; + db.close(); + + request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + event = yield undefined; + + let db2 = event.target.result; + db2.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; + + // Create index. + event.target.transaction.objectStore("foo").createIndex("foo", "num"); + yield undefined; + + // Make sure our object made it into the index. + let seenCount = 0; + + + db2.transaction("foo").objectStore("foo").index("foo") + .openKeyCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, data.num, "Good key"); + is(cursor.primaryKey, data.id, "Good value"); + seenCount++; + cursor.continue(); + } + else { + continueToNextStep(); + } + }; + yield undefined; + + is(seenCount, 1, "Saw our entry"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_create_locale_aware_index.js b/dom/indexedDB/test/unit/test_create_locale_aware_index.js new file mode 100644 index 000000000..7fc7a4ab5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_locale_aware_index.js @@ -0,0 +1,123 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreInfo = [ + { name: "a", options: { keyPath: "id", autoIncrement: true } }, + { name: "b", options: { keyPath: "id", autoIncrement: false } }, + ]; + const indexInfo = [ + { name: "1", keyPath: "unique_value", options: { unique: true, locale: "es-ES" } }, + { name: "2", keyPath: "unique_value", options: { unique: true, locale: null } }, + { name: "3", keyPath: "value", options: { unique: false, locale: "es-ES" } }, + { name: "4", keyPath: "value", options: { unique: false, locale: "es-ES" } }, + { name: "", keyPath: "value", options: { unique: false, locale: "es-ES" } }, + { name: null, keyPath: "value", options: { unique: false, locale: "es-ES" } }, + { name: undefined, keyPath: "value", options: { unique: false, locale: "es-ES" } }, + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + let objectStore = info.hasOwnProperty("options") ? + db.createObjectStore(info.name, info.options) : + db.createObjectStore(info.name); + + try { + request = objectStore.createIndex("Hola"); + ok(false, "createIndex with no keyPath should throw"); + } + catch(e) { + ok(true, "createIndex with no keyPath should throw"); + } + + let ex; + try { + objectStore.createIndex("Hola", ["foo"], { multiEntry: true }); + } + catch(e) { + ex = e; + } + ok(ex, "createIndex with array keyPath and multiEntry should throw"); + is(ex.name, "InvalidAccessError", "should throw right exception"); + ok(ex instanceof DOMException, "should throw right exception"); + is(ex.code, DOMException.INVALID_ACCESS_ERR, "should throw right exception"); + + try { + objectStore.createIndex("foo", "bar", 10); + ok(false, "createIndex with bad options should throw"); + } + catch(e) { + ok(true, "createIndex with bad options threw"); + } + + ok(objectStore.createIndex("foo", "bar", { foo: "" }), + "createIndex with unknown options should not throw"); + objectStore.deleteIndex("foo"); + + // Test index creation, and that it ends up in indexNames. + let objectStoreName = info.name; + for (let j = 0; j < indexInfo.length; j++) { + let info = indexInfo[j]; + let count = objectStore.indexNames.length; + let index = info.hasOwnProperty("options") ? + objectStore.createIndex(info.name, info.keyPath, + info.options) : + objectStore.createIndex(info.name, info.keyPath); + + let name = info.name; + if (name === null) { + name = "null"; + } + else if (name === undefined) { + name = "undefined"; + } + + is(index.name, name, "correct name"); + is(index.keyPath, info.keyPath, "correct keyPath"); + is(index.unique, info.options.unique, "correct uniqueness"); + is(index.locale, info.options.locale, "correct locale"); + + is(objectStore.indexNames.length, count + 1, + "indexNames grew in size"); + let found = false; + for (let k = 0; k < objectStore.indexNames.length; k++) { + if (objectStore.indexNames.item(k) == name) { + found = true; + break; + } + } + ok(found, "Name is on objectStore.indexNames"); + + ok(event.target.transaction, "event has a transaction"); + ok(event.target.transaction.db === db, + "transaction has the right db"); + is(event.target.transaction.mode, "versionchange", + "transaction has the correct mode"); + is(event.target.transaction.objectStoreNames.length, i + 1, + "transaction only has one object store"); + ok(event.target.transaction.objectStoreNames.contains(objectStoreName), + "transaction has the correct object store"); + } + } + + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + + event = yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_create_objectStore.js b/dom/indexedDB/test/unit/test_create_objectStore.js new file mode 100644 index 000000000..215723c26 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_objectStore.js @@ -0,0 +1,134 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreInfo = [ + { name: "1", options: { keyPath: null } }, + { name: "2", options: { keyPath: null, autoIncrement: true } }, + { name: "3", options: { keyPath: null, autoIncrement: false } }, + { name: "4", options: { keyPath: null } }, + { name: "5", options: { keyPath: "foo" } }, + { name: "6" }, + { name: "7", options: null }, + { name: "8", options: { autoIncrement: true } }, + { name: "9", options: { autoIncrement: false } }, + { name: "10", options: { keyPath: "foo", autoIncrement: false } }, + { name: "11", options: { keyPath: "foo", autoIncrement: true } }, + { name: "" }, + { name: null }, + { name: undefined } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + + let count = db.objectStoreNames.length; + is(count, 0, "correct objectStoreNames length"); + + try { + db.createObjectStore("foo", "bar"); + ok(false, "createObjectStore with bad options should throw"); + } + catch(e) { + ok(true, "createObjectStore with bad options"); + } + + ok(db.createObjectStore("foo", { foo: "" }), + "createObjectStore with unknown options should not throw"); + db.deleteObjectStore("foo"); + + for (let index in objectStoreInfo) { + index = parseInt(index); + const info = objectStoreInfo[index]; + + let objectStore = info.hasOwnProperty("options") ? + db.createObjectStore(info.name, info.options) : + db.createObjectStore(info.name); + + is(db.objectStoreNames.length, index + 1, + "updated objectStoreNames list"); + + let name = info.name; + if (name === null) { + name = "null"; + } + else if (name === undefined) { + name = "undefined"; + } + + let found = false; + for (let i = 0; i <= index; i++) { + if (db.objectStoreNames.item(i) == name) { + found = true; + break; + } + } + is(found, true, "objectStoreNames contains name"); + + is(objectStore.name, name, "Bad name"); + is(objectStore.keyPath, info.options && info.options.keyPath ? + info.options.keyPath : null, + "Bad keyPath"); + if(objectStore.indexNames.length, 0, "Bad indexNames"); + + ok(event.target.transaction, "event has a transaction"); + ok(event.target.transaction.db === db, "transaction has the right db"); + is(event.target.transaction.mode, "versionchange", + "transaction has the correct mode"); + is(event.target.transaction.objectStoreNames.length, index + 1, + "transaction has correct objectStoreNames list"); + found = false; + for (let j = 0; j < event.target.transaction.objectStoreNames.length; + j++) { + if (event.target.transaction.objectStoreNames.item(j) == name) { + found = true; + break; + } + } + is(found, true, "transaction has correct objectStoreNames list"); + } + + // Can't handle autoincrement and empty keypath + let ex; + try { + db.createObjectStore("storefail", { keyPath: "", autoIncrement: true }); + } + catch(e) { + ex = e; + } + ok(ex, "createObjectStore with empty keyPath and autoIncrement should throw"); + is(ex.name, "InvalidAccessError", "should throw right exception"); + ok(ex instanceof DOMException, "should throw right exception"); + is(ex.code, DOMException.INVALID_ACCESS_ERR, "should throw right exception"); + + // Can't handle autoincrement and array keypath + try { + db.createObjectStore("storefail", { keyPath: ["a"], autoIncrement: true }); + } + catch(e) { + ex = e; + } + ok(ex, "createObjectStore with array keyPath and autoIncrement should throw"); + is(ex.name, "InvalidAccessError", "should throw right exception"); + ok(ex instanceof DOMException, "should throw right exception"); + is(ex.code, DOMException.INVALID_ACCESS_ERR, "should throw right exception"); + + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + + event = yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_cursor_cycle.js b/dom/indexedDB/test/unit/test_cursor_cycle.js new file mode 100644 index 000000000..795a9b63d --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursor_cycle.js @@ -0,0 +1,41 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const Bob = { ss: "237-23-7732", name: "Bob" }; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + event.target.onsuccess = continueToNextStep; + + let objectStore = db.createObjectStore("foo", { keyPath: "ss" }); + objectStore.createIndex("name", "name", { unique: true }); + objectStore.add(Bob); + yield undefined; + + db.transaction("foo", "readwrite").objectStore("foo") + .index("name").openCursor().onsuccess = function(event) { + event.target.transaction.oncomplete = continueToNextStep; + let cursor = event.target.result; + if (cursor) { + let objectStore = event.target.transaction.objectStore("foo"); + objectStore.delete(Bob.ss) + .onsuccess = function(event) { cursor.continue(); }; + } + }; + yield undefined; + finishTest(); + + objectStore = null; // Bug 943409 workaround. + + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_cursor_mutation.js b/dom/indexedDB/test/unit/test_cursor_mutation.js new file mode 100644 index 000000000..13e859891 --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursor_mutation.js @@ -0,0 +1,118 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const objectStoreData = [ + // This one will be removed. + { ss: "237-23-7732", name: "Bob" }, + + // These will always be included. + { ss: "237-23-7733", name: "Ann" }, + { ss: "237-23-7734", name: "Ron" }, + { ss: "237-23-7735", name: "Sue" }, + { ss: "237-23-7736", name: "Joe" }, + + // This one will be added. + { ss: "237-23-7737", name: "Pat" } + ]; + + // Post-add and post-remove data ordered by name. + const objectStoreDataNameSort = [ 1, 4, 5, 2, 3 ]; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + event.target.onsuccess = continueToNextStep; + + let objectStore = db.createObjectStore("foo", { keyPath: "ss" }); + objectStore.createIndex("name", "name", { unique: true }); + + for (let i = 0; i < objectStoreData.length - 1; i++) { + objectStore.add(objectStoreData[i]); + } + yield undefined; + + let count = 0; + + let sawAdded = false; + let sawRemoved = false; + + db.transaction("foo").objectStore("foo").openCursor().onsuccess = + function(event) { + event.target.transaction.oncomplete = continueToNextStep; + let cursor = event.target.result; + if (cursor) { + if (cursor.value.name == objectStoreData[0].name) { + sawRemoved = true; + } + if (cursor.value.name == + objectStoreData[objectStoreData.length - 1].name) { + sawAdded = true; + } + cursor.continue(); + count++; + } + }; + yield undefined; + + is(count, objectStoreData.length - 1, "Good initial count"); + is(sawAdded, false, "Didn't see item that is about to be added"); + is(sawRemoved, true, "Saw item that is about to be removed"); + + count = 0; + sawAdded = false; + sawRemoved = false; + + db.transaction("foo", "readwrite").objectStore("foo") + .index("name").openCursor().onsuccess = function(event) { + event.target.transaction.oncomplete = continueToNextStep; + let cursor = event.target.result; + if (cursor) { + if (cursor.value.name == objectStoreData[0].name) { + sawRemoved = true; + } + if (cursor.value.name == + objectStoreData[objectStoreData.length - 1].name) { + sawAdded = true; + } + + is(cursor.value.name, + objectStoreData[objectStoreDataNameSort[count++]].name, + "Correct name"); + + if (count == 1) { + let objectStore = event.target.transaction.objectStore("foo"); + objectStore.delete(objectStoreData[0].ss) + .onsuccess = function(event) { + objectStore.add(objectStoreData[objectStoreData.length - 1]) + .onsuccess = + function(event) { + cursor.continue(); + }; + }; + } + else { + cursor.continue(); + } + } + }; + yield undefined; + + is(count, objectStoreData.length - 1, "Good final count"); + is(sawAdded, true, "Saw item that was added"); + is(sawRemoved, false, "Didn't see item that was removed"); + + finishTest(); + + objectStore = null; // Bug 943409 workaround. + + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_cursor_update_updates_indexes.js b/dom/indexedDB/test/unit/test_cursor_update_updates_indexes.js new file mode 100644 index 000000000..1ea0bc883 --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursor_update_updates_indexes.js @@ -0,0 +1,99 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const START_DATA = "hi"; + const END_DATA = "bye"; + const objectStoreInfo = [ + { name: "1", options: { keyPath: null }, key: 1, + entry: { data: START_DATA } }, + { name: "2", options: { keyPath: "foo" }, + entry: { foo: 1, data: START_DATA } }, + { name: "3", options: { keyPath: null, autoIncrement: true }, + entry: { data: START_DATA } }, + { name: "4", options: { keyPath: "foo", autoIncrement: true }, + entry: { data: START_DATA } }, + ]; + + for (let i = 0; i < objectStoreInfo.length; i++) { + // Create our object stores. + let info = objectStoreInfo[i]; + + ok(true, "1"); + request = indexedDB.open(name, i + 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let db = event.target.result; + + ok(true, "2"); + let objectStore = info.hasOwnProperty("options") ? + db.createObjectStore(info.name, info.options) : + db.createObjectStore(info.name); + + // Create the indexes on 'data' on the object store. + let index = objectStore.createIndex("data_index", "data", + { unique: false }); + let uniqueIndex = objectStore.createIndex("unique_data_index", "data", + { unique: true }); + // Populate the object store with one entry of data. + request = info.hasOwnProperty("key") ? + objectStore.add(info.entry, info.key) : + objectStore.add(info.entry); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + // Use a cursor to update 'data' to END_DATA. + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "4"); + + let cursor = request.result; + let obj = cursor.value; + obj.data = END_DATA; + request = cursor.update(obj); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "5"); + + // Check both indexes to make sure that they were updated. + request = index.get(END_DATA); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "6"); + ok(obj.data, event.target.result.data, + "Non-unique index was properly updated."); + + request = uniqueIndex.get(END_DATA); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "7"); + ok(obj.data, event.target.result.data, + "Unique index was properly updated."); + + // Wait for success + yield undefined; + + db.close(); + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_cursors.js b/dom/indexedDB/test/unit/test_cursors.js new file mode 100644 index 000000000..48d1eff83 --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursors.js @@ -0,0 +1,383 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const keys = [1, -1, 0, 10, 2000, "q", "z", "two", "b", "a"]; + const sortedKeys = [-1, 0, 1, 10, 2000, "a", "b", "q", "two", "z"]; + + is(keys.length, sortedKeys.length, "Good key setup"); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore("autoIncrement", + { autoIncrement: true }); + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + ok(!event.target.result, "No results"); + testGenerator.next(); + } + yield undefined; + + objectStore = db.createObjectStore("autoIncrementKeyPath", + { keyPath: "foo", + autoIncrement: true }); + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + ok(!event.target.result, "No results"); + testGenerator.next(); + } + yield undefined; + + objectStore = db.createObjectStore("keyPath", { keyPath: "foo" }); + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + ok(!event.target.result, "No results"); + testGenerator.next(); + } + yield undefined; + + objectStore = db.createObjectStore("foo"); + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + ok(!event.target.result, "No results"); + testGenerator.next(); + } + yield undefined; + + let keyIndex = 0; + + for (let i in keys) { + request = objectStore.add("foo", keys[i]); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++keyIndex == keys.length) { + testGenerator.next(); + } + }; + } + yield undefined; + + keyIndex = 0; + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + cursor.continue(); + + try { + cursor.continue(); + ok(false, "continue twice should throw"); + } + catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + is(e.code, DOMException.INVALID_STATE_ERR, "correct code"); + } + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, keys.length, "Saw all added items"); + + keyIndex = 4; + + let range = IDBKeyRange.bound(2000, "q"); + request = objectStore.openCursor(range); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + cursor.continue(); + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 8, "Saw all the expected keys"); + + keyIndex = 0; + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + if (keyIndex) { + cursor.continue(); + } + else { + cursor.continue("b"); + } + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + keyIndex += keyIndex ? 1: 6; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, keys.length, "Saw all the expected keys"); + + keyIndex = 0; + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + if (keyIndex) { + cursor.continue(); + } + else { + cursor.continue(10); + } + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + keyIndex += keyIndex ? 1: 3; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, keys.length, "Saw all the expected keys"); + + keyIndex = 0; + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + if (keyIndex) { + cursor.continue(); + } + else { + cursor.continue("c"); + } + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + keyIndex += keyIndex ? 1 : 7; + } + else { + ok(cursor === null, "The request result should be null."); + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, keys.length, "Saw all the expected keys"); + + keyIndex = 0; + + request = objectStore.openCursor(); + request.onerror = errorHandler; + let storedCursor = null; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + storedCursor = cursor; + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + if (keyIndex == 4) { + request = cursor.update("bar"); + request.onerror = errorHandler; + request.onsuccess = function(event) { + keyIndex++; + cursor.continue(); + }; + } + else { + keyIndex++; + cursor.continue(); + } + } + else { + ok(cursor === null, "The request result should be null."); + ok(storedCursor.value === undefined, "The cursor's value should be undefined."); + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, keys.length, "Saw all the expected keys"); + + request = objectStore.get(sortedKeys[4]); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, "bar", "Update succeeded"); + + request = objectStore.put("foo", sortedKeys[4]); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + keyIndex = 0; + + let gotRemoveEvent = false; + let retval = false; + + request = objectStore.openCursor(null, "next"); + request.onerror = errorHandler; + storedCursor = null; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + storedCursor = cursor; + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + if (keyIndex == 4) { + request = cursor.delete(); + request.onerror = errorHandler; + request.onsuccess = function(event) { + ok(event.target.result === undefined, "Should be undefined"); + is(keyIndex, 5, "Got result of remove before next continue"); + gotRemoveEvent = true; + }; + } + + keyIndex++; + cursor.continue(); + } + else { + ok(cursor === null, "The request result should be null."); + ok(storedCursor.value === undefined, "The cursor's value should be undefined."); + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, keys.length, "Saw all the expected keys"); + is(gotRemoveEvent, true, "Saw the remove event"); + + request = objectStore.get(sortedKeys[4]); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Entry was deleted"); + + request = objectStore.add("foo", sortedKeys[4]); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + keyIndex = sortedKeys.length - 1; + + request = objectStore.openCursor(null, "prev"); + request.onerror = errorHandler; + storedCursor = null; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + storedCursor = cursor; + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + cursor.continue(); + + is(cursor.key, sortedKeys[keyIndex], "Correct key"); + is(cursor.primaryKey, sortedKeys[keyIndex], "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + + keyIndex--; + } + else { + ok(cursor === null, "The request result should be null."); + ok(storedCursor.value === undefined, "The cursor's value should be undefined."); + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all added items"); + + // Wait for success + yield undefined; + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_database_close_without_onclose.js b/dom/indexedDB/test/unit/test_database_close_without_onclose.js new file mode 100644 index 000000000..e4ba62335 --- /dev/null +++ b/dom/indexedDB/test/unit/test_database_close_without_onclose.js @@ -0,0 +1,49 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : + "test_database_close_without_onclose.js"; + + const checkpointSleepTimeSec = 10; + + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + + ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + let db = event.target.result; + db.createObjectStore("store"); + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, openRequest, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + db.onclose = errorHandler; + + db.close(); + setTimeout(continueToNextStepSync, checkpointSleepTimeSec * 1000); + yield undefined; + + ok(true, "The close event should not be fired after closed normally!"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_database_onclose.js b/dom/indexedDB/test/unit/test_database_onclose.js new file mode 100644 index 000000000..28650d835 --- /dev/null +++ b/dom/indexedDB/test/unit/test_database_onclose.js @@ -0,0 +1,245 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + function testInvalidStateError(aDb, aTxn) { + try { + info("The db shall become invalid after closed."); + aDb.transaction("store"); + ok(false, "InvalidStateError shall be thrown."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + } + + try { + info("The txn shall become invalid after closed."); + aTxn.objectStore("store"); + ok(false, "InvalidStateError shall be thrown."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + } + } + + const name = this.window ? window.location.pathname : + "test_database_onclose.js"; + + info("#1: Verifying IDBDatabase.onclose after cleared by the agent."); + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + + ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + let db = event.target.result; + db.createObjectStore("store"); + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, openRequest, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + let txn = db.transaction("store", "readwrite"); + let objectStore = txn.objectStore("store"); + + clearAllDatabases(continueToNextStep); + + db.onclose = grabEventAndContinueHandler; + event = yield undefined; + is(event.type, "close", "Expect a close event"); + is(event.target, db, "Correct target"); + + info("Wait for callback of clearAllDatabases()."); + yield undefined; + + testInvalidStateError(db, txn); + + info("#2: Verifying IDBDatabase.onclose && IDBTransaction.onerror " + + "in *write* operation after cleared by the agent."); + openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + + ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + db = event.target.result; + db.createObjectStore("store"); + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, openRequest, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + txn = db.transaction("store", "readwrite"); + objectStore = txn.objectStore("store"); + + let objectId = 0; + while(true) { + let addRequest = objectStore.add({foo: "foo"}, objectId); + addRequest.onerror = function(event) { + info("addRequest.onerror, objectId: " + objectId); + txn.onerror = grabEventAndContinueHandler; + testGenerator.send(true); + } + addRequest.onsuccess = function() { + testGenerator.send(false); + } + + if (objectId == 0) { + clearAllDatabases(() => { + info("clearAllDatabases is done."); + continueToNextStep(); + }); + } + + objectId++; + + let aborted = yield undefined; + if (aborted) { + break; + } + } + + event = yield undefined; + is(event.type, "error", "Got an error event"); + is(event.target.error.name, "AbortError", "Expected AbortError was thrown."); + event.preventDefault(); + + txn.onabort = grabEventAndContinueHandler; + event = yield undefined; + is(event.type, "abort", "Got an abort event"); + is(event.target.error.name, "AbortError", "Expected AbortError was thrown."); + + db.onclose = grabEventAndContinueHandler; + event = yield undefined; + is(event.type, "close", "Expect a close event"); + is(event.target, db, "Correct target"); + testInvalidStateError(db, txn); + + info("Wait for the callback of clearAllDatabases()."); + yield undefined; + + info("#3: Verifying IDBDatabase.onclose && IDBTransaction.onerror " + + "in *read* operation after cleared by the agent."); + openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + + ok(openRequest instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + db = event.target.result; + objectStore = + db.createObjectStore("store", { keyPath: "id", autoIncrement: true }); + // The number of read records varies between 1~2000 before the db is cleared + // during testing. + let numberOfObjects = 3000; + objectId = 0; + while(true) { + let addRequest = objectStore.add({foo: "foo"}); + addRequest.onsuccess = function() { + objectId++; + testGenerator.send(objectId == numberOfObjects); + } + addRequest.onerror = errorHandler; + + let done = yield undefined; + if (done) { + break; + } + } + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, openRequest, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + txn = db.transaction("store"); + objectStore = txn.objectStore("store"); + + let numberOfReadObjects = 0; + let readRequest = objectStore.openCursor(); + readRequest.onerror = function(event) { + info("readRequest.onerror, numberOfReadObjects: " + numberOfReadObjects); + testGenerator.send(true); + } + readRequest.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + numberOfReadObjects++; + event.target.result.continue(); + } else { + info("Cursor is invalid, numberOfReadObjects: " + numberOfReadObjects); + todo(false, "All records are iterated before database is cleared!"); + testGenerator.send(false); + } + } + + clearAllDatabases(() => { + info("clearAllDatabases is done."); + continueToNextStep(); + }); + + readRequestError = yield undefined; + if (readRequestError) { + txn.onerror = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "error", "Got an error event"); + is(event.target.error.name, "AbortError", "Expected AbortError was thrown."); + event.preventDefault(); + + txn.onabort = grabEventAndContinueHandler; + event = yield undefined; + is(event.type, "abort", "Got an abort event"); + is(event.target.error.name, "AbortError", "Expected AbortError was thrown."); + + db.onclose = grabEventAndContinueHandler; + event = yield undefined; + is(event.type, "close", "Expect a close event"); + is(event.target, db, "Correct target"); + + testInvalidStateError(db, txn); + } + + info("Wait for the callback of clearAllDatabases()."); + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_defaultStorageUpgrade.js b/dom/indexedDB/test/unit/test_defaultStorageUpgrade.js new file mode 100644 index 000000000..ce87f6dac --- /dev/null +++ b/dom/indexedDB/test/unit/test_defaultStorageUpgrade.js @@ -0,0 +1,160 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const openParams = [ + // This one lives in storage/default/http+++localhost + { url: "http://localhost", dbName: "dbA", dbVersion: 1 }, + + // This one lives in storage/default/http+++www.mozilla.org + { url: "http://www.mozilla.org", dbName: "dbB", dbVersion: 1 }, + + // This one lives in storage/default/http+++www.mozilla.org+8080 + { url: "http://www.mozilla.org:8080", dbName: "dbC", dbVersion: 1 }, + + // This one lives in storage/default/https+++www.mozilla.org + { url: "https://www.mozilla.org", dbName: "dbD", dbVersion: 1 }, + + // This one lives in storage/default/https+++www.mozilla.org+8080 + { url: "https://www.mozilla.org:8080", dbName: "dbE", dbVersion: 1 }, + + // This one lives in storage/permanent/indexeddb+++fx-devtools + { url: "indexeddb://fx-devtools", dbName: "dbF", + dbOptions: { version: 1, storage: "persistent" } }, + + // This one lives in storage/permanent/moz-safe-about+home + { url: "moz-safe-about:home", dbName: "dbG", + dbOptions: { version: 1, storage: "persistent" } }, + + // This one lives in storage/default/file++++Users+joe+ + { url: "file:///Users/joe/", dbName: "dbH", dbVersion: 1 }, + + // This one lives in storage/default/file++++Users+joe+index.html + { url: "file:///Users/joe/index.html", dbName: "dbI", dbVersion: 1 }, + + // This one lives in storage/default/file++++c++Users+joe+ + { url: "file:///c:/Users/joe/", dbName: "dbJ", dbVersion: 1 }, + + // This one lives in storage/default/file++++c++Users+joe+index.html + { url: "file:///c:/Users/joe/index.html", dbName: "dbK", dbVersion: 1 }, + + // This one lives in storage/permanent/chrome + { dbName: "dbL", dbVersion: 1 }, + + // This one lives in storage/default/1007+t+https+++developer.cdn.mozilla.net + { appId: 1007, inIsolatedMozBrowser: true, url: "https://developer.cdn.mozilla.net", + dbName: "dbN", dbVersion: 1 }, + + // This one lives in storage/default/http+++127.0.0.1 + { url: "http://127.0.0.1", dbName: "dbO", dbVersion: 1 }, + + // This one lives in storage/default/file++++ + { url: "file:///", dbName: "dbP", dbVersion: 1 }, + + // This one lives in storage/default/file++++c++ + { url: "file:///c:/", dbName: "dbQ", dbVersion: 1 }, + + // This one lives in storage/default/file++++Users+joe+c+++index.html + { url: "file:///Users/joe/c++/index.html", dbName: "dbR", dbVersion: 1 }, + + // This one lives in storage/default/file++++Users+joe+c+++index.html + { url: "file:///Users/joe/c///index.html", dbName: "dbR", dbVersion: 1 }, + + // This one lives in storage/default/file++++++index.html + { url: "file:///+/index.html", dbName: "dbS", dbVersion: 1 }, + + // This one lives in storage/default/file++++++index.html + { url: "file://///index.html", dbName: "dbS", dbVersion: 1 }, + + // This one lives in storage/permanent/resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data + { url: "resource://fx-share-addon-at-mozilla-dot-org-fx-share-addon-data", + dbName: "dbU", dbOptions: { version: 1, storage: "persistent" } }, + + // This one lives in storage/temporary/http+++localhost+81 + // The .metadata file was intentionally removed for this origin directory + // to test restoring during upgrade. + { url: "http://localhost:81", dbName: "dbV", + dbOptions: { version: 1, storage: "temporary" } }, + + // This one lives in storage/temporary/http+++localhost+82 + // The .metadata file was intentionally truncated for this origin directory + // to test restoring during upgrade. + { url: "http://localhost:82", dbName: "dbW", + dbOptions: { version: 1, storage: "temporary" } }, + + // This one lives in storage/temporary/1007+t+https+++developer.cdn.mozilla.net + { appId: 1007, inIsolatedMozBrowser: true, url: "https://developer.cdn.mozilla.net", + dbName: "dbY", dbOptions: { version: 1, storage: "temporary" } }, + + // This one lives in storage/temporary/http+++localhost + { url: "http://localhost", dbName: "dbZ", + dbOptions: { version: 1, storage: "temporary" } } + ]; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase(params) { + let request; + if ("url" in params) { + let uri = ios.newURI(params.url, null, null); + let principal = + ssm.createCodebasePrincipal(uri, + {appId: params.appId || ssm.NO_APPID, + inIsolatedMozBrowser: params.inIsolatedMozBrowser}); + if ("dbVersion" in params) { + request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbVersion); + } else { + request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbOptions); + } + } else { + if ("dbVersion" in params) { + request = indexedDB.open(params.dbName, params.dbVersion); + } else { + request = indexedDB.open(params.dbName, params.dbOptions); + } + } + return request; + } + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("defaultStorageUpgrade_profile"); + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_deleteDatabase.js b/dom/indexedDB/test/unit/test_deleteDatabase.js new file mode 100644 index 000000000..7c9f76522 --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase.js @@ -0,0 +1,106 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + ok(indexedDB.deleteDatabase, "deleteDatabase function should exist!"); + + let request = indexedDB.open(name, 10); + request.onerror = errorHandler; + request.onsuccess = unexpectedSuccessHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + + ok(request instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + let db = event.target.result; + db.createObjectStore("stuff"); + + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, request, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + request = indexedDB.open(name, 10); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "success", "Expect a success event"); + is(event.target, request, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + let db2 = event.target.result; + is(db2.objectStoreNames.length, 1, "Expect an objectStore here"); + + var onversionchangecalled = false; + + function closeDBs(event) { + onversionchangecalled = true; + ok(event instanceof IDBVersionChangeEvent, "expect a versionchange event"); + is(event.oldVersion, 10, "oldVersion should be 10"); + ok(event.newVersion === null, "newVersion should be null"); + ok(!(event.newVersion === undefined), "newVersion should be null"); + ok(!(event.newVersion === 0), "newVersion should be null"); + db.close(); + db2.close(); + db.onversionchange = unexpectedSuccessHandler; + db2.onversionchange = unexpectedSuccessHandler; + }; + + // The IDB spec doesn't guarantee the order that onversionchange will fire + // on the dbs. + db.onversionchange = closeDBs; + db2.onversionchange = closeDBs; + + request = indexedDB.deleteDatabase(name); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + ok(request instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + event = yield undefined; + ok(onversionchangecalled, "Expected versionchange events"); + is(event.type, "success", "expect a success event"); + is(event.target, request, "event has right target"); + ok(event.target.result === undefined, "event should have no result"); + + request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.target.result.version, 1, "DB has proper version"); + is(event.target.result.objectStoreNames.length, 0, "DB should have no object stores"); + + + request = indexedDB.deleteDatabase("thisDatabaseHadBetterNotExist"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + ok(true, "deleteDatabase on a non-existent database succeeded"); + + request = indexedDB.open("thisDatabaseHadBetterNotExist"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + ok(true, "after deleting a non-existent database, open should work"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_deleteDatabase_interactions.js b/dom/indexedDB/test/unit/test_deleteDatabase_interactions.js new file mode 100644 index 000000000..87f6a6d20 --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase_interactions.js @@ -0,0 +1,62 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 10); + request.onerror = errorHandler; + request.onsuccess = unexpectedSuccessHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + + ok(request instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + let db = event.target.result; + db.createObjectStore("stuff"); + + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, request, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + db.close(); + + request = indexedDB.deleteDatabase(name); + + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + ok(request instanceof IDBOpenDBRequest, "Expect an IDBOpenDBRequest"); + + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + + event = yield undefined; + is(event.type, "success", "expect a success event"); + is(event.target, request, "event has right target"); + is(event.target.result, undefined, "event should have no result"); + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.target.result.version, 1, "DB has proper version"); + is(event.target.result.objectStoreNames.length, 0, "DB should have no object stores"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_deleteDatabase_onblocked.js b/dom/indexedDB/test/unit/test_deleteDatabase_onblocked.js new file mode 100644 index 000000000..51390612d --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase_onblocked.js @@ -0,0 +1,83 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const dbVersion = 10; + + let openRequest = indexedDB.open(name, dbVersion); + openRequest.onerror = errorHandler; + openRequest.onblocked = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + let db = event.target.result; + db.onversionchange = errorHandler; + db.createObjectStore("stuff"); + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, openRequest, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + db.onversionchange = grabEventAndContinueHandler; + let deletingRequest = indexedDB.deleteDatabase(name); + deletingRequest.onerror = errorHandler; + deletingRequest.onsuccess = errorHandler; + deletingRequest.onblocked = errorHandler; + + event = yield undefined; + + is(event.type, "versionchange", "Expect an versionchange event"); + is(event.target, db, "Event has right target"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + is(event.oldVersion, dbVersion, "Correct old version"); + is(event.newVersion, null, "Correct new version"); + + deletingRequest.onblocked = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "blocked", "Expect an blocked event"); + is(event.target, deletingRequest, "Event has right target"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + is(event.oldVersion, dbVersion, "Correct old version"); + is(event.newVersion, null, "Correct new version"); + + deletingRequest.onsuccess = grabEventAndContinueHandler; + db.close(); + + event = yield undefined; + + is(event.type, "success", "expect a success event"); + is(event.target, deletingRequest, "event has right target"); + is(event.target.result, undefined, "event should have no result"); + + openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + db = event.target.result; + is(db.version, 1, "DB has proper version"); + is(db.objectStoreNames.length, 0, "DB should have no object stores"); + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_deleteDatabase_onblocked_duringVersionChange.js b/dom/indexedDB/test/unit/test_deleteDatabase_onblocked_duringVersionChange.js new file mode 100644 index 000000000..40ece64c0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase_onblocked_duringVersionChange.js @@ -0,0 +1,84 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const dbVersion = 10; + + let openRequest = indexedDB.open(name, dbVersion); + openRequest.onerror = errorHandler; + openRequest.onblocked = errorHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + + let db = event.target.result; + db.onversionchange = errorHandler; + db.createObjectStore("stuff"); + + let deletingRequest = indexedDB.deleteDatabase(name); + deletingRequest.onerror = errorHandler; + deletingRequest.onsuccess = errorHandler; + deletingRequest.onblocked = errorHandler; + + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "success", "Expect a success event"); + is(event.target, openRequest, "Event has right target"); + ok(event.target.result instanceof IDBDatabase, "Result should be a database"); + is(db.objectStoreNames.length, 1, "Expect an objectStore here"); + + db.onversionchange = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "versionchange", "Expect an versionchange event"); + is(event.target, db, "Event has right target"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + is(event.oldVersion, dbVersion, "Correct old version"); + is(event.newVersion, null, "Correct new version"); + + deletingRequest.onblocked = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.type, "blocked", "Expect an blocked event"); + is(event.target, deletingRequest, "Event has right target"); + ok(event instanceof IDBVersionChangeEvent, "Expect a versionchange event"); + is(event.oldVersion, dbVersion, "Correct old version"); + is(event.newVersion, null, "Correct new version"); + + deletingRequest.onsuccess = grabEventAndContinueHandler; + db.close(); + + event = yield undefined; + + is(event.type, "success", "expect a success event"); + is(event.target, deletingRequest, "event has right target"); + is(event.target.result, undefined, "event should have no result"); + + openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + db = event.target.result; + is(db.version, 1, "DB has proper version"); + is(db.objectStoreNames.length, 0, "DB should have no object stores"); + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_event_source.js b/dom/indexedDB/test/unit/test_event_source.js new file mode 100644 index 000000000..232a13f0d --- /dev/null +++ b/dom/indexedDB/test/unit/test_event_source.js @@ -0,0 +1,36 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "Objects"; + + var request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + var event = yield undefined; + + is(event.target.source, null, "correct event.target.source"); + + var db = event.target.result; + var objectStore = db.createObjectStore(objectStoreName, + { autoIncrement: true }); + request = objectStore.add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.source === objectStore, "correct event.source"); + + // Wait for success + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_file_copy_failure.js b/dom/indexedDB/test/unit/test_file_copy_failure.js new file mode 100644 index 000000000..586149233 --- /dev/null +++ b/dom/indexedDB/test/unit/test_file_copy_failure.js @@ -0,0 +1,75 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = "test_file_copy_failure.js"; + const objectStoreName = "Blobs"; + const blob = getBlob(getView(1024)); + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Creating orphaned file"); + + let filesDir = getChromeFilesDir(); + + let journalFile = filesDir.clone(); + journalFile.append("journals"); + journalFile.append("1"); + + let exists = journalFile.exists(); + ok(!exists, "Journal file doesn't exist"); + + journalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8)); + + let file = filesDir.clone(); + file.append("1"); + + exists = file.exists(); + ok(!exists, "File doesn't exist"); + + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8)); + + info("Storing blob"); + + let trans = db.transaction(objectStoreName, "readwrite"); + + request = trans.objectStore(objectStoreName).add(blob, 1); + request.onsuccess = continueToNextStepSync; + + yield undefined; + + trans.oncomplete = continueToNextStepSync; + + yield undefined; + + exists = journalFile.exists(); + ok(!exists, "Journal file doesn't exist"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_filehandle_append_read_data.js b/dom/indexedDB/test/unit/test_filehandle_append_read_data.js new file mode 100644 index 000000000..ed2f77ef6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_filehandle_append_read_data.js @@ -0,0 +1,98 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "FileHandle doesn't work in workers yet"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + var testString = "Lorem ipsum his ponderum delicatissimi ne, at noster dolores urbanitas pro, cibo elaboraret no his. Ea dicunt maiorum usu. Ad appareat facilisis mediocritatem eos. Tale graeci mentitum in eos, hinc insolens at nam. Graecis nominavi aliquyam eu vix. Id solet assentior sadipscing pro. Et per atqui graecis, usu quot viris repudiandae ei, mollis evertitur an nam. At nam dolor ignota, liber labore omnesque ea mei, has movet voluptaria in. Vel an impetus omittantur. Vim movet option salutandi ex, ne mei ignota corrumpit. Mucius comprehensam id per. Est ea putant maiestatis."; + for (let i = 0; i < 5; i++) { + testString += testString; + } + + var testBuffer = getRandomBuffer(100000); + + var testBlob = new Blob([testBuffer], {type: "binary/random"}); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.createMutableFile("test.txt"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let mutableFile = event.target.result; + mutableFile.onerror = errorHandler; + + let location = 0; + + let fileHandle = mutableFile.open("readwrite"); + is(fileHandle.location, location, "Correct location"); + + request = fileHandle.append(testString); + ok(fileHandle.location === null, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + fileHandle.location = 0; + request = fileHandle.readAsText(testString.length); + location += testString.length + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let resultString = event.target.result; + ok(resultString == testString, "Correct string data"); + + request = fileHandle.append(testBuffer); + ok(fileHandle.location === null, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + fileHandle.location = location; + request = fileHandle.readAsArrayBuffer(testBuffer.byteLength); + location += testBuffer.byteLength; + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let resultBuffer = event.target.result; + ok(compareBuffers(resultBuffer, testBuffer), "Correct array buffer data"); + + request = fileHandle.append(testBlob); + ok(fileHandle.location === null, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + fileHandle.location = location; + request = fileHandle.readAsArrayBuffer(testBlob.size); + location += testBlob.size; + is(fileHandle.location, location, "Correct location"); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + resultBuffer = event.target.result; + ok(compareBuffers(resultBuffer, testBuffer), "Correct blob data"); + + request = fileHandle.getMetadata({ size: true }); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + is(result.size, location, "Correct size"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_getAll.js b/dom/indexedDB/test/unit/test_getAll.js new file mode 100644 index 000000000..c543feb07 --- /dev/null +++ b/dom/indexedDB/test/unit/test_getAll.js @@ -0,0 +1,195 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const values = [ "a", "1", 1, "foo", 300, true, false, 4.5, null ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request.onsuccess = grabEventAndContinueHandler; + request = objectStore.mozGetAll(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 0, "No elements"); + + let addedCount = 0; + + for (let i in values) { + request = objectStore.add(values[i]); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedCount == values.length) { + executeSoon(function() { testGenerator.next(); }); + } + } + } + yield undefined; + yield undefined; + + request = db.transaction("foo").objectStore("foo").mozGetAll(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, values.length, "Same length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[i], "Same value"); + } + + request = db.transaction("foo").objectStore("foo").mozGetAll(null, 5); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 5, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[i], "Same value"); + } + + let keyRange = IDBKeyRange.bound(1, 9); + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, values.length, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[i], "Same value"); + } + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange, 0); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, values.length, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[i], "Same value"); + } + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange, null); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, values.length, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[i], "Same value"); + } + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange, undefined); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, values.length, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[i], "Same value"); + } + + keyRange = IDBKeyRange.bound(4, 7); + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[parseInt(i) + 3], "Same value"); + } + + // Get should take a key range also but it doesn't return an array. + request = db.transaction("foo").objectStore("foo").get(keyRange); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, false, "Not an array object"); + is(event.target.result, values[3], "Correct value"); + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange, 2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[parseInt(i) + 3], "Same value"); + } + + keyRange = IDBKeyRange.bound(4, 7); + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange, 50); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[parseInt(i) + 3], "Same value"); + } + + keyRange = IDBKeyRange.bound(4, 7); + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange, 0); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + keyRange = IDBKeyRange.bound(4, 7, true, true); + + request = db.transaction("foo").objectStore("foo").mozGetAll(keyRange); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], values[parseInt(i) + 4], "Same value"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_getUsage.js b/dom/indexedDB/test/unit/test_getUsage.js new file mode 100644 index 000000000..1834b1815 --- /dev/null +++ b/dom/indexedDB/test/unit/test_getUsage.js @@ -0,0 +1,128 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const origins = [ + { + origin: "http://example.com", + persisted: false, + usage: 49152 + }, + + { + origin: "http://localhost", + persisted: false, + usage: 147456 + }, + + { + origin: "http://www.mozilla.org", + persisted: false, + usage: 98304 + } + ]; + + const allOrigins = [ + { + origin: "chrome", + persisted: false, + usage: 147456 + }, + + { + origin: "http://example.com", + persisted: false, + usage: 49152 + }, + + { + origin: "http://localhost", + persisted: false, + usage: 147456 + }, + + { + origin: "http://www.mozilla.org", + persisted: false, + usage: 98304 + } + ]; + + function verifyResult(result, origins) { + ok(result instanceof Array, "Got an array object"); + ok(result.length == origins.length, "Correct number of elements"); + + info("Sorting elements"); + + result.sort(function(a, b) { + let originA = a.origin + let originB = b.origin + + if (originA < originB) { + return -1; + } + if (originA > originB) { + return 1; + } + return 0; + }); + + info("Verifying elements"); + + for (let i = 0; i < result.length; i++) { + let a = result[i]; + let b = origins[i]; + ok(a.origin == b.origin, "Origin equals"); + ok(a.persisted == b.persisted, "Persisted equals"); + ok(a.usage == b.usage, "Usage equals"); + } + } + + info("Clearing"); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ true); + let result = yield undefined; + + info("Verifying result"); + + verifyResult(result, []); + + info("Installing profile"); + + // The profile contains IndexedDB databases placed across the repositories. + // The file create_db.js in the package was run locally, specifically it was + // temporarily added to xpcshell.ini and then executed: + // mach xpcshell-test --interactive dom/indexedDB/test/unit/create_db.js + installPackagedProfile("getUsage_profile"); + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ false); + result = yield undefined; + + info("Verifying result"); + + verifyResult(result, origins); + + info("Getting usage"); + + getUsage(grabResultAndContinueHandler, /* getAll */ true); + result = yield undefined; + + info("Verifying result"); + + verifyResult(result, allOrigins); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_globalObjects_ipc.js b/dom/indexedDB/test/unit/test_globalObjects_ipc.js new file mode 100644 index 000000000..641af0feb --- /dev/null +++ b/dom/indexedDB/test/unit/test_globalObjects_ipc.js @@ -0,0 +1,19 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + // Test for IDBKeyRange and indexedDB availability in ipcshell. + run_test_in_child("./GlobalObjectsChild.js", function() { + do_test_finished(); + continueToNextStep(); + }); + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_globalObjects_other.js b/dom/indexedDB/test/unit/test_globalObjects_other.js new file mode 100644 index 000000000..a1338459c --- /dev/null +++ b/dom/indexedDB/test/unit/test_globalObjects_other.js @@ -0,0 +1,60 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let ioService = + Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + function getSpec(filename) { + let file = do_get_file(filename); + let uri = ioService.newFileURI(file); + return uri.spec; + } + + // Test for IDBKeyRange and indexedDB availability in JS modules. + Cu.import(getSpec("GlobalObjectsModule.jsm")); + let test = new GlobalObjectsModule(); + test.ok = ok; + test.finishTest = continueToNextStep; + test.runTest(); + yield undefined; + + // Test for IDBKeyRange and indexedDB availability in JS components. + do_load_manifest("GlobalObjectsComponent.manifest"); + test = Cc["@mozilla.org/dom/indexeddb/GlobalObjectsComponent;1"]. + createInstance(Ci.nsISupports).wrappedJSObject; + test.ok = ok; + test.finishTest = continueToNextStep; + test.runTest(); + yield undefined; + + // Test for IDBKeyRange and indexedDB availability in JS sandboxes. + let principal = Cc["@mozilla.org/systemprincipal;1"]. + createInstance(Ci.nsIPrincipal); + let sandbox = new Cu.Sandbox(principal, + { wantGlobalProperties: ["indexedDB"] }); + sandbox.__SCRIPT_URI_SPEC__ = getSpec("GlobalObjectsSandbox.js"); + Cu.evalInSandbox( + "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \ + .createInstance(Components.interfaces.mozIJSSubScriptLoader) \ + .loadSubScript(__SCRIPT_URI_SPEC__);", sandbox, "1.7"); + sandbox.ok = ok; + sandbox.finishTest = continueToNextStep; + Cu.evalInSandbox("runTest();", sandbox); + yield undefined; + + finishTest(); + yield undefined; +} + +this.runTest = function() { + do_get_profile(); + + do_test_pending(); + testGenerator.next(); +} diff --git a/dom/indexedDB/test/unit/test_globalObjects_xpc.js b/dom/indexedDB/test/unit/test_globalObjects_xpc.js new file mode 100644 index 000000000..57611d046 --- /dev/null +++ b/dom/indexedDB/test/unit/test_globalObjects_xpc.js @@ -0,0 +1,26 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = "Splendid Test"; + + // Test for IDBKeyRange and indexedDB availability in xpcshell. + let keyRange = IDBKeyRange.only(42); + ok(keyRange, "Got keyRange"); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + ok(db, "Got database"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_global_data.js b/dom/indexedDB/test/unit/test_global_data.js new file mode 100644 index 000000000..e7df96ec9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_global_data.js @@ -0,0 +1,57 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStore = { name: "Objects", + options: { keyPath: "id", autoIncrement: true } }; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db1 = event.target.result; + + is(db1.objectStoreNames.length, 0, "No objectStores in db1"); + + db1.createObjectStore(objectStore.name, objectStore.options); + + continueToNextStep(); + yield undefined; + + request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let db2 = event.target.result; + + ok(db1 !== db2, "Databases are not the same object"); + + is(db1.objectStoreNames.length, 1, "1 objectStore in db1"); + is(db1.objectStoreNames.item(0), objectStore.name, "Correct name"); + + is(db2.objectStoreNames.length, 1, "1 objectStore in db2"); + is(db2.objectStoreNames.item(0), objectStore.name, "Correct name"); + + let objectStore1 = db1.transaction(objectStore.name) + .objectStore(objectStore.name); + is(objectStore1.name, objectStore.name, "Same name"); + is(objectStore1.keyPath, objectStore.options.keyPath, "Same keyPath"); + + let objectStore2 = db2.transaction(objectStore.name) + .objectStore(objectStore.name); + + ok(objectStore1 !== objectStore2, "Different objectStores"); + is(objectStore1.name, objectStore2.name, "Same name"); + is(objectStore1.keyPath, objectStore2.keyPath, "Same keyPath"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_idbSubdirUpgrade.js b/dom/indexedDB/test/unit/test_idbSubdirUpgrade.js new file mode 100644 index 000000000..1e793b391 --- /dev/null +++ b/dom/indexedDB/test/unit/test_idbSubdirUpgrade.js @@ -0,0 +1,68 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const openParams = [ + // This one lives in storage/default/http+++www.mozilla.org + { url: "http://www.mozilla.org", dbName: "dbB", dbVersion: 1 }, + + // This one lives in storage/default/1007+t+https+++developer.cdn.mozilla.net + { appId: 1007, inIsolatedMozBrowser: true, url: "https://developer.cdn.mozilla.net", + dbName: "dbN", dbVersion: 1 }, + ]; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase(params) { + let uri = ios.newURI(params.url, null, null); + let principal = + ssm.createCodebasePrincipal(uri, + {appId: params.appId || ssm.NO_APPID, + inIsolatedMozBrowser: params.inIsolatedMozBrowser}); + let request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbVersion); + return request; + } + + for (let i = 1; i <= 2; i++) { + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("idbSubdirUpgrade" + i + "_profile"); + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_idle_maintenance.js b/dom/indexedDB/test/unit/test_idle_maintenance.js new file mode 100644 index 000000000..04e3b2d80 --- /dev/null +++ b/dom/indexedDB/test/unit/test_idle_maintenance.js @@ -0,0 +1,174 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let uri = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newURI("https://www.example.com", null, null); + let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + let principal = ssm.createCodebasePrincipal(uri, {}); + + info("Setting permissions"); + + let permMgr = + Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); + permMgr.add(uri, "indexedDB", Ci.nsIPermissionManager.ALLOW_ACTION); + + info("Setting idle preferences to prevent real 'idle-daily' notification"); + + let prefs = + Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + prefs.setIntPref("idle.lastDailyNotification", (Date.now() / 1000) - 10); + + info("Activating real idle service"); + + do_get_idle(); + + info("Creating databases"); + + let quotaManagerService = Cc["@mozilla.org/dom/quota-manager-service;1"]. + getService(Ci.nsIQuotaManagerService); + + // Keep at least one database open. + let req = indexedDB.open("foo-a", 1); + req.onerror = errorHandler; + req.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let dbA = event.target.result; + + // Keep at least one factory operation alive by deleting a database that is + // stil open. + req = indexedDB.open("foo-b", 1); + req.onerror = errorHandler; + req.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let dbB = event.target.result; + + indexedDB.deleteDatabase("foo-b"); + + // Create a database which we will later try to open while maintenance is + // performed. + req = indexedDB.open("foo-c", 1); + req.onerror = errorHandler; + req.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let dbC = event.target.result; + dbC.close(); + + let dbCount = 0; + + for (let persistence of ["persistent", "temporary", "default"]) { + for (let i = 1; i <= 5; i++) { + let dbName = "foo-" + i; + let dbPersistence = persistence; + let req = indexedDB.openForPrincipal(principal, + dbName, + { version: 1, + storage: dbPersistence }); + req.onerror = event => { + if (dbPersistence != "persistent") { + errorHandler(event); + return; + } + + // Explicit persistence is currently blocked on mobile. + info("Failed to create persistent database '" + dbPersistence + "/" + + dbName + "', hopefully this is on mobile!"); + + event.preventDefault(); + + if (!(--dbCount)) { + continueToNextStep(); + } + }; + req.onupgradeneeded = event => { + let db = event.target.result; + let objectStore = db.createObjectStore("foo"); + + // Add lots of data... + for (let j = 0; j < 100; j++) { + objectStore.add("abcdefghijklmnopqrstuvwxyz0123456789", j); + } + + // And then clear it so that maintenance has some space to reclaim. + objectStore.clear(); + }; + req.onsuccess = event => { + let db = event.target.result; + ok(db, "Created database '" + dbPersistence + "/" + dbName + "'"); + + db.close(); + + if (!(--dbCount)) { + continueToNextStep(); + } + }; + dbCount++; + } + } + yield undefined; + + info("Getting usage before maintenance"); + + let usageBeforeMaintenance; + + quotaManagerService.getUsageForPrincipal(principal, (request) => { + let usage = request.result.usage; + ok(usage > 0, "Usage is non-zero"); + usageBeforeMaintenance = usage; + continueToNextStep(); + }); + yield undefined; + + info("Sending fake 'idle-daily' notification to QuotaManager"); + + let observer = quotaManagerService.QueryInterface(Ci.nsIObserver); + observer.observe(null, "idle-daily", ""); + + info("Opening database while maintenance is performed"); + + req = indexedDB.open("foo-c", 1); + req.onerror = errorHandler; + req.onsuccess = grabEventAndContinueHandler; + yield undefined; + + info("Waiting for maintenance to start"); + + // This time is totally arbitrary. Most likely directory scanning will have + // completed, QuotaManager locks will be acquired, and maintenance tasks will + // be scheduled before this time has elapsed, so we will be testing the + // maintenance code. However, if something is slow then this will test + // shutting down in the middle of maintenance. + setTimeout(continueToNextStep, 10000); + yield undefined; + + info("Getting usage after maintenance"); + + let usageAfterMaintenance; + + quotaManagerService.getUsageForPrincipal(principal, (request) => { + let usage = request.result.usage; + ok(usage > 0, "Usage is non-zero"); + usageAfterMaintenance = usage; + continueToNextStep(); + }); + yield undefined; + + info("Usage before: " + usageBeforeMaintenance + ". " + + "Usage after: " + usageAfterMaintenance); + + ok(usageAfterMaintenance <= usageBeforeMaintenance, + "Maintenance decreased file sizes or left them the same"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_index_empty_keyPath.js b/dom/indexedDB/test/unit/test_index_empty_keyPath.js new file mode 100644 index 000000000..9fbcfc9c9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_empty_keyPath.js @@ -0,0 +1,83 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStoreData = [ + { key: "1", value: "foo" }, + { key: "2", value: "bar" }, + { key: "3", value: "baz" } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; // upgradeneeded + + let db = event.target.result; + + let objectStore = db.createObjectStore("data", { keyPath: null }); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; // testGenerator.send + + // Now create the index. + objectStore.createIndex("set", "", { unique: true }); + yield undefined; // success + + let trans = db.transaction("data", "readwrite"); + objectStore = trans.objectStore("data"); + index = objectStore.index("set"); + + request = index.get("bar"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.target.result, "bar", "Got correct result"); + + request = objectStore.add("foopy", 4); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + yield undefined; + + request = index.get("foopy"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + is(event.target.result, "foopy", "Got correct result"); + + request = objectStore.add("foopy", 5); + request.addEventListener("error", new ExpectError("ConstraintError", true)); + request.onsuccess = unexpectedSuccessHandler; + + trans.oncomplete = grabEventAndContinueHandler; + + yield undefined; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_index_getAll.js b/dom/indexedDB/test/unit/test_index_getAll.js new file mode 100644 index 000000000..b4af8fd04 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_getAll.js @@ -0,0 +1,191 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } } + ]; + + const indexData = [ + { name: "name", keyPath: "name", options: { unique: true } }, + { name: "height", keyPath: "height", options: { unique: false } }, + { name: "weight", keyPath: "weight", options: { unique: false } } + ]; + + const objectStoreDataNameSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + const objectStoreDataHeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + yield undefined; + ok(true, "1"); + + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + + ok(true, "2"); + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + request = objectStore.index("height").mozGetAllKeys(65); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, 0); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, null); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, undefined); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "4"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, objectStoreDataHeightSort.length, + "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[i].key, "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(null, 4); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "5"); + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[i].key, "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "6"); + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 1, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_index_getAllObjects.js b/dom/indexedDB/test/unit/test_index_getAllObjects.js new file mode 100644 index 000000000..1aad5dc08 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_getAllObjects.js @@ -0,0 +1,233 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } } + ]; + + const indexData = [ + { name: "name", keyPath: "name", options: { unique: true } }, + { name: "height", keyPath: "height", options: { unique: false } }, + { name: "weight", keyPath: "weight", options: { unique: false } } + ]; + + const objectStoreDataNameSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + const objectStoreDataHeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, {}); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; + + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + request = objectStore.index("height").mozGetAll(65); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, 0); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, null); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, undefined); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, objectStoreDataHeightSort.length, + "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[i].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(null, 4); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[i].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 1, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_index_object_cursors.js b/dom/indexedDB/test/unit/test_index_object_cursors.js new file mode 100644 index 000000000..f3daa433b --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_object_cursors.js @@ -0,0 +1,147 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const objectStoreData = [ + { name: "", options: { keyPath: "id", autoIncrement: true } }, + { name: null, options: { keyPath: "ss" } }, + { name: undefined, options: { } }, + { name: "4", options: { autoIncrement: true } }, + ]; + + const indexData = [ + { name: "", keyPath: "name", options: { unique: true } }, + { name: null, keyPath: "height", options: { } } + ]; + + const data = [ + { ss: "237-23-7732", name: "Ann", height: 60 }, + { ss: "237-23-7733", name: "Bob", height: 65 } + ]; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; + + for (let objectStoreIndex in objectStoreData) { + const objectStoreInfo = objectStoreData[objectStoreIndex]; + let objectStore = db.createObjectStore(objectStoreInfo.name, + objectStoreInfo.options); + for (let indexIndex in indexData) { + const indexInfo = indexData[indexIndex]; + let index = objectStore.createIndex(indexInfo.name, + indexInfo.keyPath, + indexInfo.options); + } + } + yield undefined; + + ok(true, "Initial setup"); + + for (let objectStoreIndex in objectStoreData) { + const info = objectStoreData[objectStoreIndex]; + + for (let indexIndex in indexData) { + const objectStoreName = objectStoreData[objectStoreIndex].name; + const indexName = indexData[indexIndex].name; + + let objectStore = + db.transaction(objectStoreName, "readwrite") + .objectStore(objectStoreName); + ok(true, "Got objectStore " + objectStoreName); + + for (let dataIndex in data) { + const obj = data[dataIndex]; + let key; + if (!info.options.keyPath && !info.options.autoIncrement) { + key = obj.ss; + } + objectStore.add(obj, key); + } + + let index = objectStore.index(indexName); + ok(true, "Got index " + indexName); + + let keyIndex = 0; + + index.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor) { + continueToNextStep(); + return; + } + + is(cursor.key, data[keyIndex][indexData[indexIndex].keyPath], + "Good key"); + is(cursor.value.ss, data[keyIndex].ss, "Correct ss"); + is(cursor.value.name, data[keyIndex].name, "Correct name"); + is(cursor.value.height, data[keyIndex].height, "Correct height"); + + if (!keyIndex) { + let obj = cursor.value; + obj.updated = true; + + cursor.update(obj).onsuccess = function(event) { + ok(true, "Object updated"); + cursor.continue(); + keyIndex++ + } + return; + } + + cursor.delete().onsuccess = function(event) { + ok(true, "Object deleted"); + cursor.continue(); + keyIndex++ + } + }; + yield undefined; + + is(keyIndex, 2, "Saw all the items"); + + keyIndex = 0; + + db.transaction(objectStoreName).objectStore(objectStoreName) + .openCursor() + .onsuccess = function(event) { + let cursor = event.target.result; + if (!cursor) { + continueToNextStep(); + return; + } + + is(cursor.value.ss, data[keyIndex].ss, "Correct ss"); + is(cursor.value.name, data[keyIndex].name, "Correct name"); + is(cursor.value.height, data[keyIndex].height, "Correct height"); + is(cursor.value.updated, true, "Correct updated flag"); + + cursor.continue(); + keyIndex++; + }; + yield undefined; + + is(keyIndex, 1, "Saw all the items"); + + db.transaction(objectStoreName, "readwrite") + .objectStore(objectStoreName).clear() + .onsuccess = continueToNextStep; + yield undefined; + + objectStore = index = null; // Bug 943409 workaround. + } + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_index_update_delete.js b/dom/indexedDB/test/unit/test_index_update_delete.js new file mode 100644 index 000000000..860087aad --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_update_delete.js @@ -0,0 +1,171 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let name = this.window ? window.location.pathname : "Splendid Test"; + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + for (let autoIncrement of [false, true]) { + let objectStore = + db.createObjectStore(autoIncrement, { keyPath: "id", + autoIncrement: autoIncrement }); + + for (let i = 0; i < 10; i++) { + objectStore.add({ id: i, index: i }); + } + + for (let unique of [false, true]) { + objectStore.createIndex(unique, "index", { unique: unique }); + } + + for (let i = 10; i < 20; i++) { + objectStore.add({ id: i, index: i }); + } + } + + event = yield undefined; + is(event.type, "success", "expect a success event"); + + for (let autoIncrement of [false, true]) { + let objectStore = db.transaction(autoIncrement) + .objectStore(autoIncrement); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.target.result, 20, "Correct number of entries in objectStore"); + + let objectStoreCount = event.target.result; + let indexCount = event.target.result; + + for (let unique of [false, true]) { + let index = db.transaction(autoIncrement, "readwrite") + .objectStore(autoIncrement) + .index(unique); + + index.count().onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.target.result, indexCount, + "Correct number of entries in index"); + + let modifiedEntry = unique ? 5 : 10; + let keyRange = IDBKeyRange.only(modifiedEntry); + + let sawEntry = false; + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + sawEntry = true; + is(cursor.key, modifiedEntry, "Correct key"); + + cursor.value.index = unique ? 30 : 35; + cursor.update(cursor.value).onsuccess = function(event) { + cursor.continue(); + } + } + else { + continueToNextStep(); + } + } + yield undefined; + + is(sawEntry, true, "Saw entry for key value " + modifiedEntry); + + // Recount index. Shouldn't change. + index = db.transaction(autoIncrement, "readwrite") + .objectStore(autoIncrement) + .index(unique); + + index.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, indexCount, + "Correct number of entries in index"); + + modifiedEntry = unique ? 30 : 35; + keyRange = IDBKeyRange.only(modifiedEntry); + + sawEntry = false; + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + sawEntry = true; + is(cursor.key, modifiedEntry, "Correct key"); + + delete cursor.value.index; + cursor.update(cursor.value).onsuccess = function(event) { + indexCount--; + cursor.continue(); + } + } + else { + continueToNextStep(); + } + } + yield undefined; + + is(sawEntry, true, "Saw entry for key value " + modifiedEntry); + + // Recount objectStore. Should be unchanged. + objectStore = db.transaction(autoIncrement, "readwrite") + .objectStore(autoIncrement); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreCount, + "Correct number of entries in objectStore"); + + // Recount index. Should be one item less. + index = objectStore.index(unique); + + index.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, indexCount, + "Correct number of entries in index"); + + modifiedEntry = objectStoreCount - 1; + + objectStore.delete(modifiedEntry).onsuccess = + grabEventAndContinueHandler; + event = yield undefined; + + objectStoreCount--; + indexCount--; + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, objectStoreCount, + "Correct number of entries in objectStore"); + + index.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, indexCount, + "Correct number of entries in index"); + + index = event = null; // Bug 943409 workaround. + } + objectStore = event = null; // Bug 943409 workaround. + } + + finishTest(); + event = db = request = null; // Bug 943409 workaround. + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_indexes.js b/dom/indexedDB/test/unit/test_indexes.js new file mode 100644 index 000000000..aaf536feb --- /dev/null +++ b/dom/indexedDB/test/unit/test_indexes.js @@ -0,0 +1,1261 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } } + ]; + + const indexData = [ + { name: "name", keyPath: "name", options: { unique: true } }, + { name: "height", keyPath: "height", options: { } }, + { name: "weight", keyPath: "weight", options: { unique: false } } + ]; + + const objectStoreDataNameSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + const objectStoreDataHeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, { keyPath: null }); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + // Check global properties to make sure they are correct. + is(objectStore.indexNames.length, indexData.length, "Good index count"); + for (let i in indexData) { + let found = false; + for (let j = 0; j < objectStore.indexNames.length; j++) { + if (objectStore.indexNames.item(j) == indexData[i].name) { + found = true; + break; + } + } + is(found, true, "objectStore has our index"); + let index = objectStore.index(indexData[i].name); + is(index.name, indexData[i].name, "Correct name"); + is(index.objectStore.name, objectStore.name, "Correct store name"); + is(index.keyPath, indexData[i].keyPath, "Correct keyPath"); + is(index.unique, indexData[i].options.unique ? true : false, + "Correct unique value"); + } + + request = objectStore.index("name").getKey("Bob"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, "237-23-7732", "Correct key returned!"); + + request = objectStore.index("name").get("Bob"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, "Bob", "Correct name returned!"); + is(event.target.result.height, 60, "Correct height returned!"); + is(event.target.result.weight, 120, "Correct weight returned!"); + + ok(true, "Test group 1"); + + let keyIndex = 0; + + request = objectStore.index("name").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + ok(!("value" in cursor), "No value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + ok(!("value" in cursor), "No value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length, "Saw all the expected keys"); + + ok(true, "Test group 2"); + + keyIndex = 0; + + request = objectStore.index("weight").openKeyCursor(null, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataWeightSort[keyIndex].value.weight, + "Correct key"); + is(cursor.primaryKey, objectStoreDataWeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataWeightSort[keyIndex].value.weight, + "Correct key"); + is(cursor.primaryKey, objectStoreDataWeightSort[keyIndex].key, + "Correct value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length - 1, "Saw all the expected keys"); + + // Check that the name index enforces its unique constraint. + objectStore = db.transaction(objectStoreName, "readwrite") + .objectStore(objectStoreName); + request = objectStore.add({ name: "Bob", height: 62, weight: 170 }, + "237-23-7738"); + request.addEventListener("error", new ExpectError("ConstraintError", true)); + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + ok(true, "Test group 3"); + + keyIndex = objectStoreDataNameSort.length - 1; + + request = objectStore.index("name").openKeyCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 4"); + + keyIndex = 1; + let keyRange = IDBKeyRange.bound("Bob", "Ron"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 5"); + + keyIndex = 2; + keyRange = IDBKeyRange.bound("Bob", "Ron", true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 6"); + + keyIndex = 1; + keyRange = IDBKeyRange.bound("Bob", "Ron", false, true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 7"); + + keyIndex = 2; + keyRange = IDBKeyRange.bound("Bob", "Ron", true, true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 8"); + + keyIndex = 1; + keyRange = IDBKeyRange.lowerBound("Bob"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 9"); + + keyIndex = 2; + keyRange = IDBKeyRange.lowerBound("Bob", true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 10"); + + keyIndex = 0; + keyRange = IDBKeyRange.upperBound("Joe"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 3, "Saw all the expected keys"); + + ok(true, "Test group 11"); + + keyIndex = 0; + keyRange = IDBKeyRange.upperBound("Joe", true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 2, "Saw all the expected keys"); + + ok(true, "Test group 12"); + + keyIndex = 3; + keyRange = IDBKeyRange.only("Pat"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 13"); + + keyIndex = 0; + + request = objectStore.index("name").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 14"); + + keyIndex = objectStoreDataNameSort.length - 1; + + request = objectStore.index("name").openCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 15"); + + keyIndex = 1; + keyRange = IDBKeyRange.bound("Bob", "Ron"); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 16"); + + keyIndex = 2; + keyRange = IDBKeyRange.bound("Bob", "Ron", true); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 17"); + + keyIndex = 1; + keyRange = IDBKeyRange.bound("Bob", "Ron", false, true); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 18"); + + keyIndex = 2; + keyRange = IDBKeyRange.bound("Bob", "Ron", true, true); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 19"); + + keyIndex = 4; + keyRange = IDBKeyRange.bound("Bob", "Ron"); + + request = objectStore.index("name").openCursor(keyRange, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 0, "Saw all the expected keys"); + + ok(true, "Test group 20"); + + // Test "nextunique" + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openKeyCursor(keyRange, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 21"); + + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openKeyCursor(keyRange, + "nextunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 21.5"); + + keyIndex = 5; + + request = objectStore.index("height").openKeyCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 22"); + + keyIndex = 5; + + request = objectStore.index("height").openKeyCursor(null, + "prevunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + if (keyIndex == 5) { + keyIndex--; + } + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 23"); + + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openCursor(keyRange, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 24"); + + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openCursor(keyRange, + "nextunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 24.5"); + + keyIndex = 5; + + request = objectStore.index("height").openCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 25"); + + keyIndex = 5; + + request = objectStore.index("height").openCursor(null, + "prevunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + if (keyIndex == 5) { + keyIndex--; + } + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 26"); + + keyIndex = 0; + + request = objectStore.index("name").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + let nextKey = !keyIndex ? "Pat" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + if (!keyIndex) { + keyIndex = 3; + } + else { + keyIndex++; + } + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length, "Saw all the expected keys"); + + ok(true, "Test group 27"); + + keyIndex = 0; + + request = objectStore.index("name").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + let nextKey = !keyIndex ? "Flo" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + keyIndex += keyIndex ? 1 : 2; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length, "Saw all the expected keys"); + + ok(true, "Test group 28"); + + keyIndex = 0; + + request = objectStore.index("name").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + let nextKey = !keyIndex ? "Pat" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + if (!keyIndex) { + keyIndex = 3; + } + else { + keyIndex++; + } + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 29"); + + keyIndex = 0; + + request = objectStore.index("name").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + let nextKey = !keyIndex ? "Flo" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex += keyIndex ? 1 : 2; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_indexes_bad_values.js b/dom/indexedDB/test/unit/test_indexes_bad_values.js new file mode 100644 index 000000000..37817c286 --- /dev/null +++ b/dom/indexedDB/test/unit/test_indexes_bad_values.js @@ -0,0 +1,130 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "Pat", height: 65 } }, + { key: "237-23-7738", value: { name: "Mel", height: 66, weight: {} } } + ]; + + const badObjectStoreData = [ + { key: "237-23-7739", value: { name: "Rob", height: 65 } }, + { key: "237-23-7740", value: { name: "Jen", height: 66, weight: {} } } + ]; + + const indexData = [ + { name: "weight", keyPath: "weight", options: { unique: false } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "Ann", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "Bob", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "Sue", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "Joe", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "Ron", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, { } ); + + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; + + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + + addedData = 0; + for (let i in badObjectStoreData) { + request = objectStore.add(badObjectStoreData[i].value, + badObjectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == badObjectStoreData.length) { + executeSoon(function() { testGenerator.next() }); + } + } + } + yield undefined; + yield undefined; + + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + let keyIndex = 0; + + request = objectStore.index("weight").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataWeightSort[keyIndex].value.weight, + "Correct key"); + is(cursor.primaryKey, objectStoreDataWeightSort[keyIndex].key, + "Correct value"); + keyIndex++; + + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataWeightSort.length, "Saw all weights"); + + keyIndex = 0; + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + keyIndex++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length + badObjectStoreData.length, + "Saw all people"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_indexes_funny_things.js b/dom/indexedDB/test/unit/test_indexes_funny_things.js new file mode 100644 index 000000000..af0384c77 --- /dev/null +++ b/dom/indexedDB/test/unit/test_indexes_funny_things.js @@ -0,0 +1,168 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + // Blob constructor is not implemented outside of windows yet (Bug 827723). + if (!this.window) { + finishTest(); + yield undefined; + } + + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStoreName = "Things"; + + const blob1 = new Blob(["foo", "bar"], { type: "text/plain" }); + const blob2 = new Blob(["foobazybar"], { type: "text/plain" }); + const blob3 = new Blob(["2"], { type: "bogus/" }); + const str = "The Book of Mozilla"; + str.type = blob1; + const arr = [1, 2, 3, 4, 5]; + + const objectStoreData = [ + { key: "1", value: blob1}, + { key: "2", value: blob2}, + { key: "3", value: blob3}, + { key: "4", value: str}, + { key: "5", value: arr}, + ]; + + const indexData = [ + { name: "type", keyPath: "type", options: { } }, + { name: "length", keyPath: "length", options: { unique: true } } + ]; + + const objectStoreDataTypeSort = [ + { key: "3", value: blob3}, + { key: "1", value: blob1}, + { key: "2", value: blob2}, + ]; + + const objectStoreDataLengthSort = [ + { key: "5", value: arr}, + { key: "4", value: str}, + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, { keyPath: null }); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + // Check global properties to make sure they are correct. + is(objectStore.indexNames.length, indexData.length, "Good index count"); + for (let i in indexData) { + let found = false; + for (let j = 0; j < objectStore.indexNames.length; j++) { + if (objectStore.indexNames.item(j) == indexData[i].name) { + found = true; + break; + } + } + is(found, true, "objectStore has our index"); + let index = objectStore.index(indexData[i].name); + is(index.name, indexData[i].name, "Correct name"); + is(index.objectStore.name, objectStore.name, "Correct store name"); + is(index.keyPath, indexData[i].keyPath, "Correct keyPath"); + is(index.unique, indexData[i].options.unique ? true : false, + "Correct unique value"); + } + + ok(true, "Test group 1"); + + let keyIndex = 0; + + request = objectStore.index("type").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataTypeSort[keyIndex].value.type, + "Correct key"); + is(cursor.primaryKey, objectStoreDataTypeSort[keyIndex].key, + "Correct primary key"); + ok(!("value" in cursor), "No value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataTypeSort[keyIndex].value.type, + "Correct key"); + is(cursor.primaryKey, objectStoreDataTypeSort[keyIndex].key, + "Correct value"); + ok(!("value" in cursor), "No value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataTypeSort.length, "Saw all the expected keys"); + + ok(true, "Test group 2"); + + keyIndex = 0; + + request = objectStore.index("length").openKeyCursor(null, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataLengthSort[keyIndex].value.length, + "Correct key"); + is(cursor.primaryKey, objectStoreDataLengthSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataLengthSort[keyIndex].value.length, + "Correct key"); + is(cursor.primaryKey, objectStoreDataLengthSort[keyIndex].key, + "Correct value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataLengthSort.length, "Saw all the expected keys"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_invalid_cursor.js b/dom/indexedDB/test/unit/test_invalid_cursor.js new file mode 100644 index 000000000..11fba3654 --- /dev/null +++ b/dom/indexedDB/test/unit/test_invalid_cursor.js @@ -0,0 +1,64 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need to implement a gc() function for worker tests"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbName = ("window" in this) ? window.location.pathname : "test"; + const dbVersion = 1; + const objectStoreName = "foo"; + const data = 0; + + let req = indexedDB.open(dbName, dbVersion); + req.onerror = errorHandler; + req.onupgradeneeded = grabEventAndContinueHandler; + req.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + let db = event.target.result; + + let objectStore = + db.createObjectStore(objectStoreName, { autoIncrement: true }); + objectStore.add(data); + + event = yield undefined; + + is(event.type, "success", "Got success event for open"); + + objectStore = db.transaction(objectStoreName).objectStore(objectStoreName); + + objectStore.openCursor().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got success event for openCursor"); + + let cursor = event.target.result; + is(cursor.value, data, "Got correct cursor value"); + + objectStore.get(cursor.key).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data, "Got correct get value"); + + info("Collecting garbage"); + + gc(); + + info("Done collecting garbage"); + + cursor.continue(); + event = yield undefined; + + is(event.target.result, null, "No more entries"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_invalid_version.js b/dom/indexedDB/test/unit/test_invalid_version.js new file mode 100644 index 000000000..7566fad8a --- /dev/null +++ b/dom/indexedDB/test/unit/test_invalid_version.js @@ -0,0 +1,50 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + try { + indexedDB.open(name, 0); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof TypeError, "Got TypeError."); + is(e.name, "TypeError", "Good error name."); + } + + try { + indexedDB.open(name, -1); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof TypeError, "Got TypeError."); + is(e.name, "TypeError", "Good error name."); + } + + try { + indexedDB.open(name, { version: 0 }); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof TypeError, "Got TypeError."); + is(e.name, "TypeError", "Good error name."); + } + + try { + indexedDB.open(name, { version: -1 }); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof TypeError, "Got TypeError."); + is(e.name, "TypeError", "Good error name."); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_invalidate.js b/dom/indexedDB/test/unit/test_invalidate.js new file mode 100644 index 000000000..fa96ae9ed --- /dev/null +++ b/dom/indexedDB/test/unit/test_invalidate.js @@ -0,0 +1,82 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const databaseName = + ("window" in this) ? window.location.pathname : "Test"; + + let dbCount = 0; + + // Test invalidating during a versionchange transaction. + info("Opening database " + ++dbCount); + + let request = indexedDB.open(databaseName, dbCount); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database " + dbCount); + + request.onupgradeneeded = unexpectedSuccessHandler; + + let objStore = + request.result.createObjectStore("foo", { autoIncrement: true }); + objStore.createIndex("fooIndex", "fooIndex", { unique: true }); + objStore.put({ foo: 1 }); + objStore.get(1); + objStore.count(); + objStore.openCursor(); + objStore.delete(1); + + info("Invalidating database " + dbCount); + + clearAllDatabases(continueToNextStepSync); + + objStore = request.result.createObjectStore("bar"); + objStore.createIndex("barIndex", "barIndex", { multiEntry: true }); + objStore.put({ bar: 1, barIndex: [ 0, 1 ] }, 10); + objStore.get(10); + objStore.count(); + objStore.openCursor(); + objStore.delete(10); + + yield undefined; + + executeSoon(continueToNextStepSync); + yield undefined; + + // Test invalidating after the complete event of a versionchange transaction. + info("Opening database " + ++dbCount); + + request = indexedDB.open(databaseName, dbCount); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database " + dbCount); + + request.onupgradeneeded = unexpectedSuccessHandler; + + request.transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", + "Got complete event for versionchange transaction on database " + dbCount); + + info("Invalidating database " + dbCount); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + executeSoon(continueToNextStepSync); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_key_requirements.js b/dom/indexedDB/test/unit/test_key_requirements.js new file mode 100644 index 000000000..90f3ce864 --- /dev/null +++ b/dom/indexedDB/test/unit/test_key_requirements.js @@ -0,0 +1,285 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.addEventListener("error", function(event) { + event.preventDefault(); + }, false); + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request = objectStore.add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let key1 = event.target.result; + + request = objectStore.put({}, key1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key1, "put gave the same key back"); + + let key2 = 10; + + request = objectStore.put({}, key2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key2, "put gave the same key back"); + + key2 = 100; + + request = objectStore.add({}, key2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key2, "put gave the same key back"); + + try { + objectStore.put({}); + ok(true, "put with no key should not throw with autoIncrement!"); + } + catch (e) { + ok(false, "put with no key threw with autoIncrement"); + } + + try { + objectStore.put({}); + ok(true, "put with no key should not throw with autoIncrement!"); + } + catch (e) { + ok(false, "put with no key threw with autoIncrement"); + } + + try { + objectStore.delete(); + ok(false, "remove with no key should throw!"); + } + catch (e) { + ok(true, "remove with no key threw"); + } + + objectStore = db.createObjectStore("bar"); + + try { + objectStore.add({}); + ok(false, "add with no key should throw!"); + } + catch (e) { + ok(true, "add with no key threw"); + } + + try { + objectStore.put({}); + ok(false, "put with no key should throw!"); + } + catch (e) { + ok(true, "put with no key threw"); + } + + try { + objectStore.put({}); + ok(false, "put with no key should throw!"); + } + catch (e) { + ok(true, "put with no key threw"); + } + + try { + objectStore.delete(); + ok(false, "remove with no key should throw!"); + } + catch (e) { + ok(true, "remove with no key threw"); + } + + objectStore = db.createObjectStore("baz", { keyPath: "id" }); + + try { + objectStore.add({}); + ok(false, "add with no key should throw!"); + } + catch (e) { + ok(true, "add with no key threw"); + } + + try { + objectStore.add({id:5}, 5); + ok(false, "add with inline key and passed key should throw!"); + } + catch (e) { + ok(true, "add with inline key and passed key threw"); + } + + try { + objectStore.put({}); + ok(false, "put with no key should throw!"); + } + catch (e) { + ok(true, "put with no key threw"); + } + + try { + objectStore.put({}); + ok(false, "put with no key should throw!"); + } + catch (e) { + ok(true, "put with no key threw"); + } + + try { + objectStore.delete(); + ok(false, "remove with no key should throw!"); + } + catch (e) { + ok(true, "remove with no key threw"); + } + + key1 = 10; + + request = objectStore.add({id:key1}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key1, "add gave back the same key"); + + request = objectStore.put({id:10}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key1, "put gave back the same key"); + + request = objectStore.put({id:10}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key1, "put gave back the same key"); + + request = objectStore.add({id:10}); + request.addEventListener("error", new ExpectError("ConstraintError", true)); + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + try { + objectStore.add({}, null); + ok(false, "add with null key should throw!"); + } + catch (e) { + ok(true, "add with null key threw"); + } + + try { + objectStore.put({}, null); + ok(false, "put with null key should throw!"); + } + catch (e) { + ok(true, "put with null key threw"); + } + + try { + objectStore.put({}, null); + ok(false, "put with null key should throw!"); + } + catch (e) { + ok(true, "put with null key threw"); + } + + try { + objectStore.delete({}, null); + ok(false, "remove with null key should throw!"); + } + catch (e) { + ok(true, "remove with null key threw"); + } + + objectStore = db.createObjectStore("bazing", { keyPath: "id", + autoIncrement: true }); + + request = objectStore.add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + key1 = event.target.result; + + request = objectStore.put({id:key1}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key1, "put gave the same key back"); + + key2 = 10; + + request = objectStore.put({id:key2}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, key2, "put gave the same key back"); + + try { + objectStore.put({}); + ok(true, "put with no key should not throw with autoIncrement!"); + } + catch (e) { + ok(false, "put with no key threw with autoIncrement"); + } + + try { + objectStore.put({}); + ok(true, "put with no key should not throw with autoIncrement!"); + } + catch (e) { + ok(false, "put with no key threw with autoIncrement"); + } + + try { + objectStore.delete(); + ok(false, "remove with no key should throw!"); + } + catch (e) { + ok(true, "remove with no key threw"); + } + + try { + objectStore.add({id:5}, 5); + ok(false, "add with inline key and passed key should throw!"); + } + catch (e) { + ok(true, "add with inline key and passed key threw"); + } + + request = objectStore.delete(key2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + // Wait for success + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_keys.js b/dom/indexedDB/test/unit/test_keys.js new file mode 100644 index 000000000..00072748e --- /dev/null +++ b/dom/indexedDB/test/unit/test_keys.js @@ -0,0 +1,269 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbname = this.window ? window.location.pathname : "Splendid Test"; + const RW = "readwrite" + let c1 = 1; + let c2 = 1; + + let openRequest = indexedDB.open(dbname, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + let trans = event.target.transaction; + + // Create test stores + let store = db.createObjectStore("store"); + + // Test simple inserts + var keys = [ + -1/0, + -1.7e308, + -10000, + -2, + -1.5, + -1, + -1.00001e-200, + -1e-200, + 0, + 1e-200, + 1.00001e-200, + 1, + 2, + 10000, + 1.7e308, + 1/0, + new Date("1750-01-02"), + new Date("1800-12-31T12:34:56.001"), + new Date(-1000), + new Date(-10), + new Date(-1), + new Date(0), + new Date(1), + new Date(2), + new Date(1000), + new Date("1971-01-01"), + new Date("1971-01-01T01:01:01Z"), + new Date("1971-01-01T01:01:01.001Z"), + new Date("1971-01-01T01:01:01.01Z"), + new Date("1971-01-01T01:01:01.1Z"), + new Date("1980-02-02"), + new Date("3333-03-19T03:33:33.333"), + "", + "\x00", + "\x00\x00", + "\x00\x01", + "\x01", + "\x02", + "\x03", + "\x04", + "\x07", + "\x08", + "\x0F", + "\x10", + "\x1F", + "\x20", + "01234", + "\x3F", + "\x40", + "A", + "A\x00", + "A1", + "ZZZZ", + "a", + "a\x00", + "aa", + "azz", + "}", + "\x7E", + "\x7F", + "\x80", + "\xFF", + "\u0100", + "\u01FF", + "\u0200", + "\u03FF", + "\u0400", + "\u07FF", + "\u0800", + "\u0FFF", + "\u1000", + "\u1FFF", + "\u2000", + "\u3FFF", + "\u4000", + "\u7FFF", + "\u8000", + "\uD800", + "\uD800a", + "\uD800\uDC01", + "\uDBFF", + "\uDC00", + "\uDFFF\uD800", + "\uFFFE", + "\uFFFF", + "\uFFFF\x00", + "\uFFFFZZZ", + [], + [-1/0], + [-1], + [0], + [1], + [1, "a"], + [1, []], + [1, [""]], + [2, 3], + [2, 3.0000000000001], + [12, [[]]], + [12, [[[]]]], + [12, [[[""]]]], + [12, [[["foo"]]]], + [12, [[[[[3]]]]]], + [12, [[[[[[3]]]]]]], + [new Date(-1)], + [new Date(1)], + [""], + ["", [[]]], + ["", [[[]]]], + ["abc"], + ["abc", "def"], + ["abc\x00"], + ["abc\x00", "\x00\x01"], + ["abc\x00", "\x00def"], + ["abc\x00\x00def"], + ["x", [[]]], + ["x", [[[]]]], + [[]], + [[],"foo"], + [[],[]], + [[[]]], + [[[]], []], + [[[]], [[]]], + [[[]], [[1]]], + [[[]], [[[]]]], + [[[1]]], + [[[[]], []]], + ]; + + for (var i = 0; i < keys.length; ++i) { + let keyI = keys[i]; + is(indexedDB.cmp(keyI, keyI), 0, i + " compared to self"); + + function doCompare(keyI) { + for (var j = i-1; j >= i-10 && j >= 0; --j) { + is(indexedDB.cmp(keyI, keys[j]), 1, i + " compared to " + j); + is(indexedDB.cmp(keys[j], keyI), -1, j + " compared to " + i); + } + } + + doCompare(keyI); + store.add(i, keyI).onsuccess = function(e) { + is(indexedDB.cmp(e.target.result, keyI), 0, + "Returned key should cmp as equal"); + ok(compareKeys(e.target.result, keyI), + "Returned key should actually be equal"); + }; + + // Test that -0 compares the same as 0 + if (keyI === 0) { + doCompare(-0); + let req = store.add(i, -0); + req.addEventListener("error", new ExpectError("ConstraintError", true)); + req.onsuccess = unexpectedSuccessHandler; + yield undefined; + } + else if (Array.isArray(keyI) && keyI.length === 1 && keyI[0] === 0) { + doCompare([-0]); + let req = store.add(i, [-0]); + req.addEventListener("error", new ExpectError("ConstraintError", true)); + req.onsuccess = unexpectedSuccessHandler; + yield undefined; + } + } + + store.openCursor().onsuccess = grabEventAndContinueHandler; + for (i = 0; i < keys.length; ++i) { + event = yield undefined; + let cursor = event.target.result; + is(indexedDB.cmp(cursor.key, keys[i]), 0, + "Read back key should cmp as equal"); + ok(compareKeys(cursor.key, keys[i]), + "Read back key should actually be equal"); + is(cursor.value, i, "Stored with right value"); + + cursor.continue(); + } + event = yield undefined; + is(event.target.result, null, "no more results expected"); + + var nan = 0/0; + var invalidKeys = [ + nan, + undefined, + null, + /x/, + {}, + new Date(NaN), + new Date("foopy"), + [nan], + [undefined], + [null], + [/x/], + [{}], + [new Date(NaN)], + [1, nan], + [1, undefined], + [1, null], + [1, /x/], + [1, {}], + [1, [nan]], + [1, [undefined]], + [1, [null]], + [1, [/x/]], + [1, [{}]], + ]; + + for (i = 0; i < invalidKeys.length; ++i) { + try { + indexedDB.cmp(invalidKeys[i], 1); + ok(false, "didn't throw"); + } + catch(ex) { + ok(ex instanceof DOMException, "Threw DOMException"); + is(ex.name, "DataError", "Threw right DOMException"); + is(ex.code, 0, "Threw with right code"); + } + try { + indexedDB.cmp(1, invalidKeys[i]); + ok(false, "didn't throw2"); + } + catch(ex) { + ok(ex instanceof DOMException, "Threw DOMException2"); + is(ex.name, "DataError", "Threw right DOMException2"); + is(ex.code, 0, "Threw with right code2"); + } + try { + store.put(1, invalidKeys[i]); + ok(false, "didn't throw3"); + } + catch(ex) { + ok(ex instanceof DOMException, "Threw DOMException3"); + is(ex.name, "DataError", "Threw right DOMException3"); + is(ex.code, 0, "Threw with right code3"); + } + } + + openRequest.onsuccess = grabEventAndContinueHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_locale_aware_index_getAll.js b/dom/indexedDB/test/unit/test_locale_aware_index_getAll.js new file mode 100644 index 000000000..c59bc127c --- /dev/null +++ b/dom/indexedDB/test/unit/test_locale_aware_index_getAll.js @@ -0,0 +1,191 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } } + ]; + + const indexData = [ + { name: "name", keyPath: "name", options: { unique: true, locale: true } }, + { name: "height", keyPath: "height", options: { unique: false, locale: true } }, + { name: "weight", keyPath: "weight", options: { unique: false, locale: true } } + ]; + + const objectStoreDataNameSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } } + ]; + + const objectStoreDataHeightSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + yield undefined; + ok(true, "1"); + + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + + ok(true, "2"); + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + request = objectStore.index("height").mozGetAllKeys(65); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, 0); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, null); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, undefined); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "3"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + ok(true, "4"); + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, objectStoreDataHeightSort.length, + "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[i].key, "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(null, 4); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "5"); + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[i].key, "Correct key"); + } + + request = objectStore.index("height").mozGetAllKeys(65, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(true, "6"); + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 1, "Correct length"); + + for (let i in event.target.result) { + is(event.target.result[i], objectStoreDataHeightSort[parseInt(i) + 3].key, + "Correct key"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_locale_aware_index_getAllObjects.js b/dom/indexedDB/test/unit/test_locale_aware_index_getAllObjects.js new file mode 100644 index 000000000..1ec82f1c2 --- /dev/null +++ b/dom/indexedDB/test/unit/test_locale_aware_index_getAllObjects.js @@ -0,0 +1,233 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } } + ]; + + const indexData = [ + { name: "name", keyPath: "name", options: { unique: true, locale: true } }, + { name: "height", keyPath: "height", options: { unique: false, locale: true } }, + { name: "weight", keyPath: "weight", options: { unique: false, locale: true } } + ]; + + const objectStoreDataNameSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } } + ]; + + const objectStoreDataHeightSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, {}); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; + + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + request = objectStore.index("height").mozGetAll(65); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, 0); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, null); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, undefined); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 2, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, objectStoreDataHeightSort.length, + "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[i].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(null, 4); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 4, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[i].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + request = objectStore.index("height").mozGetAll(65, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array object"); + is(event.target.result.length, 1, "Correct length"); + + for (let i in event.target.result) { + let result = event.target.result[i]; + let testObj = objectStoreDataHeightSort[parseInt(i) + 3].value; + + is(result.name, testObj.name, "Correct name"); + is(result.height, testObj.height, "Correct height"); + + if (testObj.hasOwnProperty("weight")) { + is(result.weight, testObj.weight, "Correct weight"); + } + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_locale_aware_indexes.js b/dom/indexedDB/test/unit/test_locale_aware_indexes.js new file mode 100644 index 000000000..b79ca58c7 --- /dev/null +++ b/dom/indexedDB/test/unit/test_locale_aware_indexes.js @@ -0,0 +1,1268 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStoreName = "People"; + + const objectStoreData = [ + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } } + ]; + + const indexData = [ + { name: "name", keyPath: "name", options: { unique: true, locale: "es-ES" } }, + { name: "height", keyPath: "height", options: { locale: "auto" } }, + { name: "weight", keyPath: "weight", options: { unique: false, locale: "es-ES" } } + ]; + + const objectStoreDataNameSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } } + ]; + + const objectStoreDataWeightSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } } + ]; + + const objectStoreDataHeightSort = [ + { key: "237-23-7733", value: { name: "ana", height: 52, weight: 110 } }, + { key: "237-23-7735", value: { name: "\u00F3scar", height: 58, weight: 130 } }, + { key: "237-23-7732", value: { name: "\u00E1na", height: 60, weight: 120 } }, + { key: "237-23-7736", value: { name: "bob", height: 65, weight: 150 } }, + { key: "237-23-7737", value: { name: "\u00E9ason", height: 65 } }, + { key: "237-23-7734", value: { name: "fabio", height: 73, weight: 180 } } + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, { keyPath: null }); + + // First, add all our data to the object store. + let addedData = 0; + for (let i in objectStoreData) { + request = objectStore.add(objectStoreData[i].value, + objectStoreData[i].key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedData == objectStoreData.length) { + testGenerator.send(event); + } + } + } + event = yield undefined; + // Now create the indexes. + for (let i in indexData) { + objectStore.createIndex(indexData[i].name, indexData[i].keyPath, + indexData[i].options); + } + is(objectStore.indexNames.length, indexData.length, "Good index count"); + yield undefined; + objectStore = db.transaction(objectStoreName) + .objectStore(objectStoreName); + + // Check global properties to make sure they are correct. + is(objectStore.indexNames.length, indexData.length, "Good index count"); + for (let i in indexData) { + let found = false; + for (let j = 0; j < objectStore.indexNames.length; j++) { + if (objectStore.indexNames.item(j) == indexData[i].name) { + found = true; + break; + } + } + is(found, true, "objectStore has our index"); + let index = objectStore.index(indexData[i].name); + is(index.name, indexData[i].name, "Correct name"); + is(index.objectStore.name, objectStore.name, "Correct store name"); + is(index.keyPath, indexData[i].keyPath, "Correct keyPath"); + is(index.unique, indexData[i].options.unique ? true : false, + "Correct unique value"); + if (indexData[i].options.locale == "auto") { + is(index.isAutoLocale, true, "Correct isAutoLocale value"); + is(index.locale, "en_US", "Correct locale value"); + } else { + is(index.isAutoLocale, false, "Correct isAutoLocale value"); + is(index.locale, indexData[i].options.locale, "Correct locale value"); + } + } + + request = objectStore.index("name").getKey("\u00E1na"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, "237-23-7732", "Correct key returned!"); + + request = objectStore.index("name").get("\u00E1na"); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, "\u00E1na", "Correct name returned!"); + is(event.target.result.height, 60, "Correct height returned!"); + is(event.target.result.weight, 120, "Correct weight returned!"); + + ok(true, "Test group 1"); + + let keyIndex = 0; + + request = objectStore.index("name").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + ok(!("value" in cursor), "No value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + ok(!("value" in cursor), "No value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length, "Saw all the expected keys"); + + ok(true, "Test group 2"); + + keyIndex = 0; + + request = objectStore.index("weight").openKeyCursor(null, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataWeightSort[keyIndex].value.weight, + "Correct key"); + is(cursor.primaryKey, objectStoreDataWeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataWeightSort[keyIndex].value.weight, + "Correct key"); + is(cursor.primaryKey, objectStoreDataWeightSort[keyIndex].key, + "Correct value"); + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length - 1, "Saw all the expected keys"); + + // Check that the name index enforces its unique constraint. + objectStore = db.transaction(objectStoreName, "readwrite") + .objectStore(objectStoreName); + request = objectStore.add({ name: "\u00E1na", height: 62, weight: 170 }, + "237-23-7738"); + request.addEventListener("error", new ExpectError("ConstraintError", true)); + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + ok(true, "Test group 3"); + + keyIndex = objectStoreDataNameSort.length - 1; + + request = objectStore.index("name").openKeyCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 4"); + + keyIndex = 1; + let keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 5"); + + keyIndex = 2; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio", true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 6"); + + keyIndex = 1; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio", false, true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 7"); + + keyIndex = 2; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio", true, true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 8"); + + keyIndex = 1; + keyRange = IDBKeyRange.lowerBound("\u00E1na"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 9"); + + keyIndex = 2; + keyRange = IDBKeyRange.lowerBound("\u00E1na", true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 10"); + + keyIndex = 0; + keyRange = IDBKeyRange.upperBound("bob"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 3, "Saw all the expected keys"); + + ok(true, "Test group 11"); + + keyIndex = 0; + keyRange = IDBKeyRange.upperBound("bob", true); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 2, "Saw all the expected keys"); + + ok(true, "Test group 12"); + + keyIndex = 3; + keyRange = IDBKeyRange.only("\u00E9ason"); + + request = objectStore.index("name").openKeyCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 13"); + + keyIndex = 0; + + request = objectStore.index("name").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 14"); + + keyIndex = objectStoreDataNameSort.length - 1; + + request = objectStore.index("name").openCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 15"); + + keyIndex = 1; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio"); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 16"); + + keyIndex = 2; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio", true); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 17"); + + keyIndex = 1; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio", false, true); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 18"); + + keyIndex = 2; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio", true, true); + + request = objectStore.index("name").openCursor(keyRange); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 19"); + + keyIndex = 4; + keyRange = IDBLocaleAwareKeyRange.bound("\u00E1na", "fabio"); + + request = objectStore.index("name").openCursor(keyRange, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 0, "Saw all the expected keys"); + + ok(true, "Test group 20"); + + // Test "nextunique" + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openKeyCursor(keyRange, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 21"); + + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openKeyCursor(keyRange, + "nextunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 21.5"); + + keyIndex = 5; + + request = objectStore.index("height").openKeyCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 22"); + + keyIndex = 5; + + request = objectStore.index("height").openKeyCursor(null, + "prevunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct value"); + + cursor.continue(); + if (keyIndex == 5) { + keyIndex--; + } + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 23"); + + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openCursor(keyRange, "next"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 5, "Saw all the expected keys"); + + ok(true, "Test group 24"); + + keyIndex = 3; + keyRange = IDBKeyRange.only(65); + + request = objectStore.index("height").openCursor(keyRange, + "nextunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + keyIndex++; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, 4, "Saw all the expected keys"); + + ok(true, "Test group 24.5"); + + keyIndex = 5; + + request = objectStore.index("height").openCursor(null, "prev"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 25"); + + keyIndex = 5; + + request = objectStore.index("height").openCursor(null, + "prevunique"); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataHeightSort[keyIndex].value.height, + "Correct key"); + is(cursor.primaryKey, objectStoreDataHeightSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataHeightSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataHeightSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataHeightSort[keyIndex].value.weight, + "Correct weight"); + } + + cursor.continue(); + if (keyIndex == 5) { + keyIndex--; + } + keyIndex--; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, -1, "Saw all the expected keys"); + + ok(true, "Test group 26"); + + keyIndex = 0; + + request = objectStore.index("name").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + let nextKey = !keyIndex ? "\u00E9ason" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + if (!keyIndex) { + keyIndex = 3; + } + else { + keyIndex++; + } + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length, "Saw all the expected keys"); + + ok(true, "Test group 27"); + + keyIndex = 0; + + request = objectStore.index("name").openKeyCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + let nextKey = !keyIndex ? "bar" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct value"); + + keyIndex += keyIndex ? 1 : 2; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreData.length, "Saw all the expected keys"); + + ok(true, "Test group 28"); + + keyIndex = 0; + + request = objectStore.index("name").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + let nextKey = !keyIndex ? "\u00E9ason" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + if (!keyIndex) { + keyIndex = 3; + } + else { + keyIndex++; + } + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + ok(true, "Test group 29"); + + keyIndex = 0; + + request = objectStore.index("name").openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + let nextKey = !keyIndex ? "bar" : undefined; + + cursor.continue(nextKey); + + is(cursor.key, objectStoreDataNameSort[keyIndex].value.name, + "Correct key"); + is(cursor.primaryKey, objectStoreDataNameSort[keyIndex].key, + "Correct primary key"); + is(cursor.value.name, objectStoreDataNameSort[keyIndex].value.name, + "Correct name"); + is(cursor.value.height, + objectStoreDataNameSort[keyIndex].value.height, + "Correct height"); + if ("weight" in cursor.value) { + is(cursor.value.weight, + objectStoreDataNameSort[keyIndex].value.weight, + "Correct weight"); + } + + keyIndex += keyIndex ? 1 : 2; + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(keyIndex, objectStoreDataNameSort.length, "Saw all the expected keys"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_lowDiskSpace.js b/dom/indexedDB/test/unit/test_lowDiskSpace.js new file mode 100644 index 000000000..eaea5797d --- /dev/null +++ b/dom/indexedDB/test/unit/test_lowDiskSpace.js @@ -0,0 +1,754 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var disableWorkerTest = "This test uses SpecialPowers"; + +var self = this; + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbName = self.window ? window.location.pathname : "test_lowDiskSpace"; + const dbVersion = 1; + + const objectStoreName = "foo"; + const objectStoreOptions = { keyPath: "foo" }; + + const indexName = "bar"; + const indexOptions = { unique: true }; + + const dbData = [ + { foo: 0, bar: 0 }, + { foo: 1, bar: 10 }, + { foo: 2, bar: 20 }, + { foo: 3, bar: 30 }, + { foo: 4, bar: 40 }, + { foo: 5, bar: 50 }, + { foo: 6, bar: 60 }, + { foo: 7, bar: 70 }, + { foo: 8, bar: 80 }, + { foo: 9, bar: 90 } + ]; + + let lowDiskMode = false; + function setLowDiskMode(val) { + let data = val ? "full" : "free"; + + if (val == lowDiskMode) { + info("Low disk mode is: " + data); + } + else { + info("Changing low disk mode to: " + data); + SpecialPowers.notifyObserversInParentProcess(null, "disk-space-watcher", + data); + lowDiskMode = val; + } + } + + { // Make sure opening works from the beginning. + info("Test 1"); + + setLowDiskMode(false); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Opened database without setting low disk mode"); + + let db = event.target.result; + db.close(); + } + + { // Make sure delete works in low disk mode. + info("Test 2"); + + setLowDiskMode(true); + + let request = indexedDB.deleteDatabase(dbName); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Deleted database after setting low disk mode"); + } + + { // Make sure creating a db in low disk mode fails. + info("Test 3"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = expectedErrorHandler("QuotaExceededError"); + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "error", "Didn't create new database in low disk mode"); + } + + { // Make sure opening an already-existing db in low disk mode succeeds. + info("Test 4"); + + setLowDiskMode(false); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Created database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + + setLowDiskMode(true); + + request = indexedDB.open(dbName); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Opened existing database in low disk mode"); + + db = event.target.result; + db.close(); + } + + { // Make sure upgrading an already-existing db in low disk mode succeeds. + info("Test 5"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion + 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Created database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + } + + { // Make sure creating objectStores in low disk mode fails. + info("Test 6"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion + 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + let txn = event.target.transaction; + txn.onerror = expectedErrorHandler("AbortError"); + txn.onabort = grabEventAndContinueHandler; + + let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions); + + request.onupgradeneeded = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "abort", "Got correct event type"); + is(event.target.error.name, "QuotaExceededError", "Got correct error type"); + + request.onerror = expectedErrorHandler("AbortError"); + event = yield undefined; + } + + { // Make sure creating indexes in low disk mode fails. + info("Test 7"); + + setLowDiskMode(false); + + let request = indexedDB.open(dbName, dbVersion + 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Upgraded database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + + setLowDiskMode(true); + + request = indexedDB.open(dbName, dbVersion + 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + db = event.target.result; + db.onerror = errorHandler; + let txn = event.target.transaction; + txn.onerror = expectedErrorHandler("AbortError"); + txn.onabort = grabEventAndContinueHandler; + + objectStore = event.target.transaction.objectStore(objectStoreName); + let index = objectStore.createIndex(indexName, indexName, indexOptions); + + request.onupgradeneeded = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "abort", "Got correct event type"); + is(event.target.error.name, "QuotaExceededError", "Got correct error type"); + + request.onerror = expectedErrorHandler("AbortError"); + event = yield undefined; + } + + { // Make sure deleting indexes in low disk mode succeeds. + info("Test 8"); + + setLowDiskMode(false); + + let request = indexedDB.open(dbName, dbVersion + 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = event.target.transaction.objectStore(objectStoreName); + let index = objectStore.createIndex(indexName, indexName, indexOptions); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Upgraded database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + + setLowDiskMode(true); + + request = indexedDB.open(dbName, dbVersion + 4); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + db = event.target.result; + db.onerror = errorHandler; + + objectStore = event.target.transaction.objectStore(objectStoreName); + objectStore.deleteIndex(indexName); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Upgraded database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + } + + { // Make sure deleting objectStores in low disk mode succeeds. + info("Test 9"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion + 5); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + db.deleteObjectStore(objectStoreName); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Upgraded database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + + // Reset everything. + indexedDB.deleteDatabase(dbName); + } + + + { // Add data that the rest of the tests will use. + info("Adding test data"); + + setLowDiskMode(false); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Upgrading database"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions); + let index = objectStore.createIndex(indexName, indexName, indexOptions); + + for (let data of dbData) { + objectStore.add(data); + } + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Upgraded database"); + ok(event.target.result === db, "Got the same database"); + + db.close(); + } + + { // Make sure read operations in readonly transactions succeed in low disk + // mode. + info("Test 10"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let transaction = db.transaction(objectStoreName); + let objectStore = transaction.objectStore(objectStoreName); + let index = objectStore.index(indexName); + + let data = dbData[0]; + + let requestCounter = new RequestCounter(); + + objectStore.get(data.foo).onsuccess = requestCounter.handler(); + objectStore.mozGetAll().onsuccess = requestCounter.handler(); + objectStore.count().onsuccess = requestCounter.handler(); + index.get(data.bar).onsuccess = requestCounter.handler(); + index.mozGetAll().onsuccess = requestCounter.handler(); + index.getKey(data.bar).onsuccess = requestCounter.handler(); + index.mozGetAllKeys().onsuccess = requestCounter.handler(); + index.count().onsuccess = requestCounter.handler(); + + let objectStoreDataCount = 0; + + request = objectStore.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + objectStoreDataCount++; + objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(objectStoreDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + let indexDataCount = 0; + + request = index.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + indexDataCount++; + indexDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(indexDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + let indexKeyDataCount = 0; + + request = index.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + indexKeyDataCount++; + indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(indexKeyDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + // Wait for all requests. + yield undefined; + + transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Transaction succeeded"); + + db.close(); + } + + { // Make sure read operations in readwrite transactions succeed in low disk + // mode. + info("Test 11"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let transaction = db.transaction(objectStoreName, "readwrite"); + let objectStore = transaction.objectStore(objectStoreName); + let index = objectStore.index(indexName); + + let data = dbData[0]; + + let requestCounter = new RequestCounter(); + + objectStore.get(data.foo).onsuccess = requestCounter.handler(); + objectStore.mozGetAll().onsuccess = requestCounter.handler(); + objectStore.count().onsuccess = requestCounter.handler(); + index.get(data.bar).onsuccess = requestCounter.handler(); + index.mozGetAll().onsuccess = requestCounter.handler(); + index.getKey(data.bar).onsuccess = requestCounter.handler(); + index.mozGetAllKeys().onsuccess = requestCounter.handler(); + index.count().onsuccess = requestCounter.handler(); + + let objectStoreDataCount = 0; + + request = objectStore.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + objectStoreDataCount++; + objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(objectStoreDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + let indexDataCount = 0; + + request = index.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + indexDataCount++; + indexDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(indexDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + let indexKeyDataCount = 0; + + request = index.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + indexKeyDataCount++; + indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(indexKeyDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + // Wait for all requests. + yield undefined; + + transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Transaction succeeded"); + + db.close(); + } + + { // Make sure write operations in readwrite transactions fail in low disk + // mode. + info("Test 12"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let transaction = db.transaction(objectStoreName, "readwrite"); + let objectStore = transaction.objectStore(objectStoreName); + let index = objectStore.index(indexName); + + let data = dbData[0]; + let newData = { foo: 999, bar: 999 }; + + let requestCounter = new RequestCounter(); + + objectStore.add(newData).onerror = requestCounter.errorHandler(); + objectStore.put(newData).onerror = requestCounter.errorHandler(); + + objectStore.get(data.foo).onsuccess = requestCounter.handler(); + objectStore.mozGetAll().onsuccess = requestCounter.handler(); + objectStore.count().onsuccess = requestCounter.handler(); + index.get(data.bar).onsuccess = requestCounter.handler(); + index.mozGetAll().onsuccess = requestCounter.handler(); + index.getKey(data.bar).onsuccess = requestCounter.handler(); + index.mozGetAllKeys().onsuccess = requestCounter.handler(); + index.count().onsuccess = requestCounter.handler(); + + let objectStoreDataCount = 0; + + request = objectStore.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + objectStoreDataCount++; + cursor.update(cursor.value).onerror = requestCounter.errorHandler(); + objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(objectStoreDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + let indexDataCount = 0; + + request = index.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + indexDataCount++; + cursor.update(cursor.value).onerror = requestCounter.errorHandler(); + indexDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(indexDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + let indexKeyDataCount = 0; + + request = index.openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + indexKeyDataCount++; + cursor.update(cursor.value).onerror = requestCounter.errorHandler(); + indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1); + } + else { + is(indexKeyDataCount, dbData.length, "Saw all data"); + requestCounter.decr(); + } + }; + requestCounter.incr(); + + // Wait for all requests. + yield undefined; + + transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Transaction succeeded"); + + db.close(); + } + + { // Make sure deleting operations in readwrite transactions succeed in low + // disk mode. + info("Test 13"); + + setLowDiskMode(true); + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let transaction = db.transaction(objectStoreName, "readwrite"); + let objectStore = transaction.objectStore(objectStoreName); + let index = objectStore.index(indexName); + + let dataIndex = 0; + let data = dbData[dataIndex++]; + + let requestCounter = new RequestCounter(); + + objectStore.delete(data.foo).onsuccess = requestCounter.handler(); + + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + cursor.delete().onsuccess = requestCounter.handler(); + } + requestCounter.decr(); + }; + requestCounter.incr(); + + index.openCursor(null, "prev").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + cursor.delete().onsuccess = requestCounter.handler(); + } + requestCounter.decr(); + }; + requestCounter.incr(); + + yield undefined; + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, dbData.length - 3, "Actually deleted something"); + + objectStore.clear(); + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, "Actually cleared"); + + transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Transaction succeeded"); + + db.close(); + } + + finishTest(); + yield undefined; +} + +function RequestCounter(expectedType) { + this._counter = 0; +} +RequestCounter.prototype = { + incr: function() { + this._counter++; + }, + + decr: function() { + if (!--this._counter) { + continueToNextStepSync(); + } + }, + + handler: function(type, preventDefault) { + this.incr(); + return function(event) { + is(event.type, type || "success", "Correct type"); + this.decr(); + }.bind(this); + }, + + errorHandler: function(eventType, errorName) { + this.incr(); + return function(event) { + is(event.type, eventType || "error", "Correct type"); + is(event.target.error.name, errorName || "QuotaExceededError", + "Correct error name"); + event.preventDefault(); + event.stopPropagation(); + this.decr(); + }.bind(this); + } +}; diff --git a/dom/indexedDB/test/unit/test_maximal_serialized_object_size.js b/dom/indexedDB/test/unit/test_maximal_serialized_object_size.js new file mode 100644 index 000000000..12a933ddc --- /dev/null +++ b/dom/indexedDB/test/unit/test_maximal_serialized_object_size.js @@ -0,0 +1,95 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need a way to set temporary prefs from a worker"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? + window.location.pathname : "test_maximal_serialized_object_size.js"; + const megaBytes = 1024 * 1024; + const kMessageOverhead = 1; // in MB + const kMaxIpcMessageSize = 20; // in MB + const kMaxIdbMessageSize = kMaxIpcMessageSize - kMessageOverhead; + + let chunks = new Array(kMaxIdbMessageSize); + for (let i = 0; i < kMaxIdbMessageSize; i++) { + chunks[i] = new ArrayBuffer(1 * megaBytes); + } + + if (this.window) { + SpecialPowers.pushPrefEnv( + { "set": [["dom.indexedDB.maxSerializedMsgSize", + kMaxIpcMessageSize * megaBytes ]] + }, + continueToNextStep + ); + yield undefined; + } else { + setMaxSerializedMsgSize(kMaxIpcMessageSize * megaBytes); + } + + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore = db.createObjectStore("test store", { keyPath: "id" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStore.name, "Correct object store name"); + + function testTooLargeError(aOperation, aObject) { + try { + objectStore[aOperation](aObject).onerror = errorHandler; + ok(false, "UnknownError is expected to be thrown!"); + } catch (e) { + ok(e instanceof DOMException, "got a DOM exception"); + is(e.name, "UnknownError", "correct error"); + ok(!!e.message, "Error message: " + e.message); + ok(e.message.startsWith("The serialized value is too large"), + "Correct error message prefix."); + } + } + + info("Verify IDBObjectStore.add() - object is too large"); + testTooLargeError("add", { id: 1, data: chunks }); + + info("Verify IDBObjectStore.add() - object size is closed to the maximal size."); + chunks.length = chunks.length - 1; + let request = objectStore.add({ id: 1, data: chunks }); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + yield undefined; + + info("Verify IDBObjectStore.add() - object key is too large"); + chunks.length = 10; + testTooLargeError("add", { id: chunks }); + + objectStore.createIndex("index name", "index"); + ok(objectStore.index("index name"), "Index created."); + + info("Verify IDBObjectStore.add() - index key is too large"); + testTooLargeError("add", { id: 2, index: chunks }); + + info("Verify IDBObjectStore.add() - object key and index key are too large"); + let indexChunks = chunks.splice(0, 5); + testTooLargeError("add", { id: chunks, index: indexChunks }); + + openRequest.onsuccess = continueToNextStep; + yield undefined; + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_metadata2Restore.js b/dom/indexedDB/test/unit/test_metadata2Restore.js new file mode 100644 index 000000000..fe29de67a --- /dev/null +++ b/dom/indexedDB/test/unit/test_metadata2Restore.js @@ -0,0 +1,268 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const openParams = [ + // This one lives in storage/permanent/chrome + // The .metadata-v2 file was intentionally removed for this origin directory + // to test restoring. + { dbName: "dbA", + dbOptions: { version: 1, storage: "persistent" } }, + + // This one lives in storage/temporary/http+++localhost + // The .metadata-v2 file was intentionally removed for this origin directory + // to test restoring. + { url: "http://localhost", dbName: "dbB", + dbOptions: { version: 1, storage: "temporary" } }, + + // This one lives in storage/default/http+++localhost+81^userContextId=1 + // The .metadata-v2 file was intentionally removed for this origin directory + // to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:81", dbName: "dbC", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+82^userContextId=1 + // The .metadata-v2 file was intentionally truncated for this origin directory + // to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:82", dbName: "dbD", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+83^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // 4 bytes of the 64 bit timestamp + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:83", dbName: "dbE", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+84^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:84", dbName: "dbF", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+85^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp and + // the 8 bit persisted flag + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:85", dbName: "dbG", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+86^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag and + // 2 bytes of the 32 bit reserved data 1 + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:86", dbName: "dbH", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+87^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag and + // the 32 bit reserved data 1 + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:87", dbName: "dbI", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+88^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1 and + // 2 bytes of the 32 bit reserved data 2 + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:88", dbName: "dbJ", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+89^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1 and + // the 32 bit reserved data 2 + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:89", dbName: "dbK", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+90^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2 and + // 2 bytes of the 32 bit suffix length + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:90", dbName: "dbL", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+91^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length and + // first 5 chars of the suffix + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:91", dbName: "dbM", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+92^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length and + // the suffix + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:92", dbName: "dbN", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+93^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length, + // the suffix and + // 2 bytes of the 32 bit group length + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:93", dbName: "dbO", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+94^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length, + // the suffix, + // the 32 bit group length and + // first 5 chars of the group + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:94", dbName: "dbP", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+95^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length, + // the suffix, + // the 32 bit group length and + // the group + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:95", dbName: "dbQ", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+96^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length, + // the suffix, + // the 32 bit group length, + // the group and + // 2 bytes of the 32 bit origin length + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:96", dbName: "dbR", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+97^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length, + // the suffix, + // the 32 bit group length, + // the group, + // the 32 bit origin length and + // first 12 char of the origin + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:97", dbName: "dbS", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+98^userContextId=1 + // The .metadata-v2 file was intentionally modified to contain only + // the 64 bit timestamp, + // the 8 bit persisted flag, + // the 32 bit reserved data 1, + // the 32 bit reserved data 2, + // the 32 bit suffix length, + // the suffix, + // the 32 bit group length, + // the group, + // the 32 bit origin length and + // the origin + // for this origin directory to test restoring. + { attrs: { userContextId: 1 }, url: "http://localhost:98", dbName: "dbT", + dbOptions: { version: 1, storage: "default" } } + ]; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase(params) { + let request; + if ("url" in params) { + let uri = ios.newURI(params.url, null, null); + let principal = ssm.createCodebasePrincipal(uri, params.attrs || {}); + request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbOptions); + } else { + request = indexedDB.open(params.dbName, params.dbOptions); + } + return request; + } + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("metadata2Restore_profile"); + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_metadataRestore.js b/dom/indexedDB/test/unit/test_metadataRestore.js new file mode 100644 index 000000000..e1a84a150 --- /dev/null +++ b/dom/indexedDB/test/unit/test_metadataRestore.js @@ -0,0 +1,109 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const openParams = [ + // This one lives in storage/permanent/chrome + { dbName: "dbA", + dbOptions: { version: 1, storage: "persistent" } }, + + // This one lives in storage/temporary/http+++localhost + { url: "http://localhost", dbName: "dbB", + dbOptions: { version: 1, storage: "temporary" } }, + + // This one lives in storage/default/http+++localhost+81 + { url: "http://localhost:81", dbName: "dbC", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+82 + { url: "http://localhost:82", dbName: "dbD", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+83 + { url: "http://localhost:83", dbName: "dbE", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+84 + { url: "http://localhost:84", dbName: "dbF", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+85 + { url: "http://localhost:85", dbName: "dbG", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+86 + { url: "http://localhost:86", dbName: "dbH", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+87 + { url: "http://localhost:87", dbName: "dbI", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+88 + { url: "http://localhost:88", dbName: "dbJ", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+89 + { url: "http://localhost:89", dbName: "dbK", + dbOptions: { version: 1, storage: "default" } }, + + // This one lives in storage/default/http+++localhost+90 + { url: "http://localhost:90", dbName: "dbL", + dbOptions: { version: 1, storage: "default" } } + ]; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase(params) { + let request; + if ("url" in params) { + let uri = ios.newURI(params.url, null, null); + let principal = ssm.createCodebasePrincipal(uri, {}); + request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbOptions); + } else { + request = indexedDB.open(params.dbName, params.dbOptions); + } + return request; + } + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("metadataRestore_profile"); + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_multientry.js b/dom/indexedDB/test/unit/test_multientry.js new file mode 100644 index 000000000..c1479498e --- /dev/null +++ b/dom/indexedDB/test/unit/test_multientry.js @@ -0,0 +1,218 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + // Test object stores + + let name = this.window ? window.location.pathname : "Splendid Test"; + let openRequest = indexedDB.open(name, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + db.onerror = errorHandler; + let tests = + [{ add: { x: 1, id: 1 }, + indexes:[{ v: 1, k: 1 }] }, + { add: { x: [2, 3], id: 2 }, + indexes:[{ v: 1, k: 1 }, + { v: 2, k: 2 }, + { v: 3, k: 2 }] }, + { put: { x: [2, 4], id: 1 }, + indexes:[{ v: 2, k: 1 }, + { v: 2, k: 2 }, + { v: 3, k: 2 }, + { v: 4, k: 1 }] }, + { add: { x: [5, 6, 5, -2, 3], id: 3 }, + indexes:[{ v:-2, k: 3 }, + { v: 2, k: 1 }, + { v: 2, k: 2 }, + { v: 3, k: 2 }, + { v: 3, k: 3 }, + { v: 4, k: 1 }, + { v: 5, k: 3 }, + { v: 6, k: 3 }] }, + { delete: IDBKeyRange.bound(1, 3), + indexes:[] }, + { put: { x: ["food", {}, false, undefined, /x/, [73, false]], id: 2 }, + indexes:[{ v: "food", k: 2 }] }, + { add: { x: [{}, /x/, -12, "food", null, [false], undefined], id: 3 }, + indexes:[{ v: -12, k: 3 }, + { v: "food", k: 2 }, + { v: "food", k: 3 }] }, + { put: { x: [], id: 2 }, + indexes:[{ v: -12, k: 3 }, + { v: "food", k: 3 }] }, + { put: { x: { y: 3 }, id: 3 }, + indexes:[] }, + { add: { x: false, id: 7 }, + indexes:[] }, + { delete: IDBKeyRange.lowerBound(0), + indexes:[] }, + ]; + + let store = db.createObjectStore("mystore", { keyPath: "id" }); + let index = store.createIndex("myindex", "x", { multiEntry: true }); + is(index.multiEntry, true, "index created with multiEntry"); + + let i; + for (i = 0; i < tests.length; ++i) { + let test = tests[i]; + let testName = " for " + JSON.stringify(test); + let req; + if (test.add) { + req = store.add(test.add); + } + else if (test.put) { + req = store.put(test.put); + } + else if (test.delete) { + req = store.delete(test.delete); + } + else { + ok(false, "borked test"); + } + req.onsuccess = grabEventAndContinueHandler; + let e = yield undefined; + + req = index.openKeyCursor(); + req.onsuccess = grabEventAndContinueHandler; + for (let j = 0; j < test.indexes.length; ++j) { + e = yield undefined; + is(req.result.key, test.indexes[j].v, "found expected index key at index " + j + testName); + is(req.result.primaryKey, test.indexes[j].k, "found expected index primary key at index " + j + testName); + req.result.continue(); + } + e = yield undefined; + ok(req.result == null, "exhausted indexes"); + + let tempIndex = store.createIndex("temp index", "x", { multiEntry: true }); + req = tempIndex.openKeyCursor(); + req.onsuccess = grabEventAndContinueHandler; + for (let j = 0; j < test.indexes.length; ++j) { + e = yield undefined; + is(req.result.key, test.indexes[j].v, "found expected temp index key at index " + j + testName); + is(req.result.primaryKey, test.indexes[j].k, "found expected temp index primary key at index " + j + testName); + req.result.continue(); + } + e = yield undefined; + ok(req.result == null, "exhausted temp index"); + store.deleteIndex("temp index"); + } + + // Unique indexes + tests = + [{ add: { x: 1, id: 1 }, + indexes:[{ v: 1, k: 1 }] }, + { add: { x: [2, 3], id: 2 }, + indexes:[{ v: 1, k: 1 }, + { v: 2, k: 2 }, + { v: 3, k: 2 }] }, + { put: { x: [2, 4], id: 3 }, + fail: true }, + { put: { x: [1, 4], id: 1 }, + indexes:[{ v: 1, k: 1 }, + { v: 2, k: 2 }, + { v: 3, k: 2 }, + { v: 4, k: 1 }] }, + { add: { x: [5, 0, 5, 5, 5], id: 3 }, + indexes:[{ v: 0, k: 3 }, + { v: 1, k: 1 }, + { v: 2, k: 2 }, + { v: 3, k: 2 }, + { v: 4, k: 1 }, + { v: 5, k: 3 }] }, + { delete: IDBKeyRange.bound(1, 2), + indexes:[{ v: 0, k: 3 }, + { v: 5, k: 3 }] }, + { add: { x: [0, 6], id: 8 }, + fail: true }, + { add: { x: 5, id: 8 }, + fail: true }, + { put: { x: 0, id: 8 }, + fail: true }, + ]; + + store.deleteIndex("myindex"); + index = store.createIndex("myindex", "x", { multiEntry: true, unique: true }); + is(index.multiEntry, true, "index created with multiEntry"); + + let indexes; + for (i = 0; i < tests.length; ++i) { + let test = tests[i]; + let testName = " for " + JSON.stringify(test); + let req; + if (test.add) { + req = store.add(test.add); + } + else if (test.put) { + req = store.put(test.put); + } + else if (test.delete) { + req = store.delete(test.delete); + } + else { + ok(false, "borked test"); + } + + if (!test.fail) { + req.onsuccess = grabEventAndContinueHandler; + let e = yield undefined; + indexes = test.indexes; + } + else { + req.onsuccess = unexpectedSuccessHandler; + req.onerror = grabEventAndContinueHandler; + ok(true, "waiting for error"); + let e = yield undefined; + ok(true, "got error: " + e.type); + e.preventDefault(); + e.stopPropagation(); + } + + let e; + req = index.openKeyCursor(); + req.onsuccess = grabEventAndContinueHandler; + for (let j = 0; j < indexes.length; ++j) { + e = yield undefined; + is(req.result.key, indexes[j].v, "found expected index key at index " + j + testName); + is(req.result.primaryKey, indexes[j].k, "found expected index primary key at index " + j + testName); + req.result.continue(); + } + e = yield undefined; + ok(req.result == null, "exhausted indexes"); + + let tempIndex = store.createIndex("temp index", "x", { multiEntry: true, unique: true }); + req = tempIndex.openKeyCursor(); + req.onsuccess = grabEventAndContinueHandler; + for (let j = 0; j < indexes.length; ++j) { + e = yield undefined; + is(req.result.key, indexes[j].v, "found expected temp index key at index " + j + testName); + is(req.result.primaryKey, indexes[j].k, "found expected temp index primary key at index " + j + testName); + req.result.continue(); + } + e = yield undefined; + ok(req.result == null, "exhausted temp index"); + store.deleteIndex("temp index"); + } + + + openRequest.onsuccess = grabEventAndContinueHandler; + yield undefined; + + let trans = db.transaction(["mystore"], "readwrite"); + store = trans.objectStore("mystore"); + index = store.index("myindex"); + is(index.multiEntry, true, "index still is multiEntry"); + trans.oncomplete = grabEventAndContinueHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_mutableFileUpgrade.js b/dom/indexedDB/test/unit/test_mutableFileUpgrade.js new file mode 100644 index 000000000..7862c7a90 --- /dev/null +++ b/dom/indexedDB/test/unit/test_mutableFileUpgrade.js @@ -0,0 +1,122 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbNames = [ + "No files", + "Blobs and mutable files" + ] + const version = 1; + const objectStoreName = "test"; + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("mutableFileUpgrade_profile"); + + let request = indexedDB.open(dbNames[0], version); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(1); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, "text", "Correct result"); + + request = indexedDB.open(dbNames[1], version); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Correct event type"); + + db = event.target.result; + db.onerror = errorHandler; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(1); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, "text", "Correct result"); + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(2); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyBlob(event.target.result, getBlob("blob0")); + yield undefined; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(3); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let result = event.target.result; + + verifyBlob(result[0], getBlob("blob1")); + yield undefined; + + verifyBlob(result[1], getBlob("blob2")); + yield undefined; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(4); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + verifyMutableFile(event.target.result, getFile("mutablefile0", "", "")); + yield undefined; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(5); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + result = event.target.result; + + verifyMutableFile(result[0], getFile("mutablefile1", "", "")); + yield undefined; + + verifyMutableFile(result[1], getFile("mutablefile2", "", "")); + yield undefined; + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(6); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + result = event.target.result; + + verifyBlob(result[0], getBlob("blob3")); + yield undefined; + + verifyMutableFile(result[1], getFile("mutablefile3", "", "")); + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_names_sorted.js b/dom/indexedDB/test/unit/test_names_sorted.js new file mode 100644 index 000000000..eac03a84a --- /dev/null +++ b/dom/indexedDB/test/unit/test_names_sorted.js @@ -0,0 +1,114 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreInfo = [ + { name: "foo", options: { keyPath: "id" }, location: 1 }, + { name: "bar", options: { keyPath: "id" }, location: 0 }, + ]; + const indexInfo = [ + { name: "foo", keyPath: "value", location: 1 }, + { name: "bar", keyPath: "value", location: 0 }, + ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + let objectStore = info.hasOwnProperty("options") ? + db.createObjectStore(info.name, info.options) : + db.createObjectStore(info.name); + + // Test index creation, and that it ends up in indexNames. + let objectStoreName = info.name; + for (let j = 0; j < indexInfo.length; j++) { + let info = indexInfo[j]; + let count = objectStore.indexNames.length; + let index = info.hasOwnProperty("options") ? + objectStore.createIndex(info.name, info.keyPath, + info.options) : + objectStore.createIndex(info.name, info.keyPath); + } + } + + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + + event = yield undefined; + + let objectStoreNames = [] + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + objectStoreNames.push(info.name); + + is(db.objectStoreNames[info.location], info.name, + "Got objectStore name in the right location"); + + let trans = db.transaction(info.name); + let objectStore = trans.objectStore(info.name); + for (let j = 0; j < indexInfo.length; j++) { + let info = indexInfo[j]; + is(objectStore.indexNames[info.location], info.name, + "Got index name in the right location"); + } + } + + let trans = db.transaction(objectStoreNames); + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + + is(trans.objectStoreNames[info.location], info.name, + "Got objectStore name in the right location"); + } + + db.close(); + + request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + + objectStoreNames = [] + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + objectStoreNames.push(info.name); + + is(db.objectStoreNames[info.location], info.name, + "Got objectStore name in the right location"); + + let trans = db.transaction(info.name); + let objectStore = trans.objectStore(info.name); + for (let j = 0; j < indexInfo.length; j++) { + let info = indexInfo[j]; + is(objectStore.indexNames[info.location], info.name, + "Got index name in the right location"); + } + } + + trans = db.transaction(objectStoreNames); + for (let i = 0; i < objectStoreInfo.length; i++) { + let info = objectStoreInfo[i]; + + is(trans.objectStoreNames[info.location], info.name, + "Got objectStore name in the right location"); + } + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_objectCursors.js b/dom/indexedDB/test/unit/test_objectCursors.js new file mode 100644 index 000000000..6c84a7394 --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectCursors.js @@ -0,0 +1,85 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStores = [ + { name: "a", autoIncrement: false }, + { name: "b", autoIncrement: true } + ]; + + const indexes = [ + { name: "a", options: { } }, + { name: "b", options: { unique: true } } + ]; + + var j = 0; + for (let i in objectStores) { + let request = indexedDB.open(name, ++j); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onversionchange = function(event) { + event.target.close(); + }; + + let objectStore = + db.createObjectStore(objectStores[i].name, + { keyPath: "id", + autoIncrement: objectStores[i].autoIncrement }); + + for (let j in indexes) { + objectStore.createIndex(indexes[j].name, "name", indexes[j].options); + } + + let data = { name: "Ben" }; + if (!objectStores[i].autoIncrement) { + data.id = 1; + } + + request = objectStore.add(data); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.result == 1 || event.target.result == 2, "Good id"); + } + + executeSoon(function() { testGenerator.next(); }); + yield undefined; + + let request = indexedDB.open(name, j); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + for (let i in objectStores) { + for (let j in indexes) { + let objectStore = db.transaction(objectStores[i].name) + .objectStore(objectStores[i].name); + let index = objectStore.index(indexes[j].name); + + request = index.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function (event) { + is(event.target.result.value.name, "Ben", "Good object"); + executeSoon(function() { testGenerator.next(); }); + } + yield undefined; + } + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_objectStore_getAllKeys.js b/dom/indexedDB/test/unit/test_objectStore_getAllKeys.js new file mode 100644 index 000000000..dfc1870c7 --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_getAllKeys.js @@ -0,0 +1,123 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() { + const dbName = this.window ? + window.location.pathname : + "test_objectStore_getAllKeys"; + const dbVersion = 1; + const objectStoreName = "foo"; + const keyCount = 200; + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + info("Creating database"); + + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName); + for (let i = 0; i < keyCount; i++) { + objectStore.add(true, i); + } + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + db = event.target.result; + objectStore = db.transaction(objectStoreName).objectStore(objectStoreName); + + info("Getting all keys"); + objectStore.getAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(Array.isArray(event.target.result), "Got an array result"); + is(event.target.result.length, keyCount, "Got correct array length"); + + let match = true; + for (let i = 0; i < keyCount; i++) { + if (event.target.result[i] != i) { + match = false; + break; + } + } + ok(match, "Got correct keys"); + + info("Getting all keys with key range"); + let keyRange = IDBKeyRange.bound(10, 20, false, true); + objectStore.getAllKeys(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(Array.isArray(event.target.result), "Got an array result"); + is(event.target.result.length, 10, "Got correct array length"); + + match = true; + for (let i = 10; i < 20; i++) { + if (event.target.result[i - 10] != i) { + match = false; + break; + } + } + ok(match, "Got correct keys"); + + info("Getting all keys with unmatched key range"); + keyRange = IDBKeyRange.bound(10000, 200000); + objectStore.getAllKeys(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(Array.isArray(event.target.result), "Got an array result"); + is(event.target.result.length, 0, "Got correct array length"); + + info("Getting all keys with limit"); + objectStore.getAllKeys(null, 5).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(Array.isArray(event.target.result), "Got an array result"); + is(event.target.result.length, 5, "Got correct array length"); + + match = true; + for (let i = 0; i < 5; i++) { + if (event.target.result[i] != i) { + match = false; + break; + } + } + ok(match, "Got correct keys"); + + info("Getting all keys with key range and limit"); + keyRange = IDBKeyRange.bound(10, 20, false, true); + objectStore.getAllKeys(keyRange, 5).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(Array.isArray(event.target.result), "Got an array result"); + is(event.target.result.length, 5, "Got correct array length"); + + match = true; + for (let i = 10; i < 15; i++) { + if (event.target.result[i - 10] != i) { + match = false; + break; + } + } + ok(match, "Got correct keys"); + + info("Getting all keys with unmatched key range and limit"); + keyRange = IDBKeyRange.bound(10000, 200000); + objectStore.getAllKeys(keyRange, 5).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(Array.isArray(event.target.result), "Got an array result"); + is(event.target.result.length, 0, "Got correct array length"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_objectStore_inline_autoincrement_key_added_on_put.js b/dom/indexedDB/test/unit/test_objectStore_inline_autoincrement_key_added_on_put.js new file mode 100644 index 000000000..176b3962d --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_inline_autoincrement_key_added_on_put.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + var request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + var event = yield undefined; + + var db = event.target.result; + + var test = { + name: "inline key; key generator", + autoIncrement: true, + storedObject: {name: "Lincoln"}, + keyName: "id", + }; + + let objectStore = db.createObjectStore(test.name, + { keyPath: test.keyName, + autoIncrement: test.autoIncrement }); + + request = objectStore.add(test.storedObject); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let id = event.target.result; + request = objectStore.get(id); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + // Sanity check! + is(event.target.result.name, test.storedObject.name, + "The correct object was stored."); + + // Ensure that the id was also stored on the object. + is(event.target.result.id, id, "The object had the id stored on it."); + + // Wait for success + yield undefined; + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_objectStore_openKeyCursor.js b/dom/indexedDB/test/unit/test_objectStore_openKeyCursor.js new file mode 100644 index 000000000..3e88cedb5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_openKeyCursor.js @@ -0,0 +1,400 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() { + const dbName = this.window ? + window.location.pathname : + "test_objectStore_openKeyCursor"; + const dbVersion = 1; + const objectStoreName = "foo"; + const keyCount = 100; + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + info("Creating database"); + + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName); + for (let i = 0; i < keyCount; i++) { + objectStore.add(true, i); + } + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + db = event.target.result; + objectStore = db.transaction(objectStoreName, "readwrite") + .objectStore(objectStoreName); + + info("Getting all keys"); + objectStore.getAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + const allKeys = event.target.result; + + ok(Array.isArray(allKeys), "Got an array result"); + is(allKeys.length, keyCount, "Got correct array length"); + + info("Opening normal key cursor"); + + let seenKeys = []; + objectStore.openKeyCursor().onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + is(cursor.source, objectStore, "Correct source"); + is(cursor.direction, "next", "Correct direction"); + + let exception = null; + try { + cursor.update(10); + } catch(e) { + exception = e; + } + ok(!!exception, "update() throws for key cursor"); + + exception = null; + try { + cursor.delete(); + } catch(e) { + exception = e; + } + ok(!!exception, "delete() throws for key cursor"); + + is(cursor.key, cursor.primaryKey, "key and primaryKey match"); + ok(!("value" in cursor), "No 'value' property on key cursor"); + + seenKeys.push(cursor.key); + cursor.continue(); + }; + yield undefined; + + is(seenKeys.length, allKeys.length, "Saw the right number of keys"); + + let match = true; + for (let i = 0; i < seenKeys.length; i++) { + if (seenKeys[i] !== allKeys[i]) { + match = false; + break; + } + } + ok(match, "All keys matched"); + + info("Opening key cursor with keyRange"); + + let keyRange = IDBKeyRange.bound(10, 20, false, true); + + seenKeys = []; + objectStore.openKeyCursor(keyRange).onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + is(cursor.source, objectStore, "Correct source"); + is(cursor.direction, "next", "Correct direction"); + + let exception = null; + try { + cursor.update(10); + } catch(e) { + exception = e; + } + ok(!!exception, "update() throws for key cursor"); + + exception = null; + try { + cursor.delete(); + } catch(e) { + exception = e; + } + ok(!!exception, "delete() throws for key cursor"); + + is(cursor.key, cursor.primaryKey, "key and primaryKey match"); + ok(!("value" in cursor), "No 'value' property on key cursor"); + + seenKeys.push(cursor.key); + cursor.continue(); + }; + yield undefined; + + is(seenKeys.length, 10, "Saw the right number of keys"); + + match = true; + for (let i = 0; i < seenKeys.length; i++) { + if (seenKeys[i] !== allKeys[i + 10]) { + match = false; + break; + } + } + ok(match, "All keys matched"); + + info("Opening key cursor with unmatched keyRange"); + + keyRange = IDBKeyRange.bound(10000, 200000); + + seenKeys = []; + objectStore.openKeyCursor(keyRange).onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + ok(false, "Shouldn't have any keys here"); + cursor.continue(); + }; + yield undefined; + + is(seenKeys.length, 0, "Saw the right number of keys"); + + info("Opening reverse key cursor"); + + seenKeys = []; + objectStore.openKeyCursor(null, "prev").onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + is(cursor.source, objectStore, "Correct source"); + is(cursor.direction, "prev", "Correct direction"); + + let exception = null; + try { + cursor.update(10); + } catch(e) { + exception = e; + } + ok(!!exception, "update() throws for key cursor"); + + exception = null; + try { + cursor.delete(); + } catch(e) { + exception = e; + } + ok(!!exception, "delete() throws for key cursor"); + + is(cursor.key, cursor.primaryKey, "key and primaryKey match"); + ok(!("value" in cursor), "No 'value' property on key cursor"); + + seenKeys.push(cursor.key); + cursor.continue(); + }; + yield undefined; + + is(seenKeys.length, allKeys.length, "Saw the right number of keys"); + + seenKeys.reverse(); + + match = true; + for (let i = 0; i < seenKeys.length; i++) { + if (seenKeys[i] !== allKeys[i]) { + match = false; + break; + } + } + ok(match, "All keys matched"); + + info("Opening reverse key cursor with key range"); + + keyRange = IDBKeyRange.bound(10, 20, false, true); + + seenKeys = []; + objectStore.openKeyCursor(keyRange, "prev").onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + is(cursor.source, objectStore, "Correct source"); + is(cursor.direction, "prev", "Correct direction"); + + let exception = null; + try { + cursor.update(10); + } catch(e) { + exception = e; + } + ok(!!exception, "update() throws for key cursor"); + + exception = null; + try { + cursor.delete(); + } catch(e) { + exception = e; + } + ok(!!exception, "delete() throws for key cursor"); + + is(cursor.key, cursor.primaryKey, "key and primaryKey match"); + ok(!("value" in cursor), "No 'value' property on key cursor"); + + seenKeys.push(cursor.key); + cursor.continue(); + }; + yield undefined; + + is(seenKeys.length, 10, "Saw the right number of keys"); + + seenKeys.reverse(); + + match = true; + for (let i = 0; i < 10; i++) { + if (seenKeys[i] !== allKeys[i + 10]) { + match = false; + break; + } + } + ok(match, "All keys matched"); + + info("Opening reverse key cursor with unmatched key range"); + + keyRange = IDBKeyRange.bound(10000, 200000); + + seenKeys = []; + objectStore.openKeyCursor(keyRange, "prev").onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + ok(false, "Shouldn't have any keys here"); + cursor.continue(); + }; + yield undefined; + + is(seenKeys.length, 0, "Saw the right number of keys"); + + info("Opening key cursor with advance"); + + seenKeys = []; + objectStore.openKeyCursor().onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + is(cursor.source, objectStore, "Correct source"); + is(cursor.direction, "next", "Correct direction"); + + let exception = null; + try { + cursor.update(10); + } catch(e) { + exception = e; + } + ok(!!exception, "update() throws for key cursor"); + + exception = null; + try { + cursor.delete(); + } catch(e) { + exception = e; + } + ok(!!exception, "delete() throws for key cursor"); + + is(cursor.key, cursor.primaryKey, "key and primaryKey match"); + ok(!("value" in cursor), "No 'value' property on key cursor"); + + seenKeys.push(cursor.key); + if (seenKeys.length == 1) { + cursor.advance(10); + } else { + cursor.continue(); + } + }; + yield undefined; + + is(seenKeys.length, allKeys.length - 9, "Saw the right number of keys"); + + match = true; + for (let i = 0, j = 0; i < seenKeys.length; i++) { + if (seenKeys[i] !== allKeys[i + j]) { + match = false; + break; + } + if (i == 0) { + j = 9; + } + } + ok(match, "All keys matched"); + + info("Opening key cursor with continue-to-key"); + + seenKeys = []; + objectStore.openKeyCursor().onsuccess = event => { + let cursor = event.target.result; + if (!cursor) { + continueToNextStepSync(); + return; + } + + is(cursor.source, objectStore, "Correct source"); + is(cursor.direction, "next", "Correct direction"); + + let exception = null; + try { + cursor.update(10); + } catch(e) { + exception = e; + } + ok(!!exception, "update() throws for key cursor"); + + exception = null; + try { + cursor.delete(); + } catch(e) { + exception = e; + } + ok(!!exception, "delete() throws for key cursor"); + + is(cursor.key, cursor.primaryKey, "key and primaryKey match"); + ok(!("value" in cursor), "No 'value' property on key cursor"); + + seenKeys.push(cursor.key); + + if (seenKeys.length == 1) { + cursor.continue(10); + } else { + cursor.continue(); + } + }; + yield undefined; + + is(seenKeys.length, allKeys.length - 9, "Saw the right number of keys"); + + match = true; + for (let i = 0, j = 0; i < seenKeys.length; i++) { + if (seenKeys[i] !== allKeys[i + j]) { + match = false; + break; + } + if (i == 0) { + j = 9; + } + } + ok(match, "All keys matched"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_objectStore_remove_values.js b/dom/indexedDB/test/unit/test_objectStore_remove_values.js new file mode 100644 index 000000000..ee0628e9d --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_remove_values.js @@ -0,0 +1,92 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + var data = [ + { name: "inline key; key generator", + autoIncrement: true, + storedObject: {name: "Lincoln"}, + keyName: "id", + keyValue: undefined, + }, + { name: "inline key; no key generator", + autoIncrement: false, + storedObject: {id: 1, name: "Lincoln"}, + keyName: "id", + keyValue: undefined, + }, + { name: "out of line key; key generator", + autoIncrement: true, + storedObject: {name: "Lincoln"}, + keyName: undefined, + keyValue: undefined, + }, + { name: "out of line key; no key generator", + autoIncrement: false, + storedObject: {name: "Lincoln"}, + keyName: null, + keyValue: 1, + } + ]; + + for (let i = 0; i < data.length; i++) { + let test = data[i]; + + let request = indexedDB.open(name, i+1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onversionchange = function(event) { + event.target.close(); + }; + + let objectStore = db.createObjectStore(test.name, + { keyPath: test.keyName, + autoIncrement: test.autoIncrement }); + + request = objectStore.add(test.storedObject, test.keyValue); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let id = event.target.result; + request = objectStore.get(id); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + // Sanity check! + is(test.storedObject.name, event.target.result.name, + "The correct object was stored."); + + request = objectStore.delete(id); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + // Make sure it was removed. + request = objectStore.get(id); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.result === undefined, "Object was deleted"); + + // Wait for success + yield undefined; + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_object_identity.js b/dom/indexedDB/test/unit/test_object_identity.js new file mode 100644 index 000000000..fa7b91339 --- /dev/null +++ b/dom/indexedDB/test/unit/test_object_identity.js @@ -0,0 +1,48 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + let transaction = event.target.transaction; + + let objectStore1 = db.createObjectStore("foo"); + let objectStore2 = transaction.objectStore("foo"); + ok(objectStore1 === objectStore2, "Got same objectStores"); + + let index1 = objectStore1.createIndex("bar", "key"); + let index2 = objectStore2.index("bar"); + ok(index1 === index2, "Got same indexes"); + + request.onsuccess = continueToNextStep; + yield undefined; + + transaction = db.transaction(db.objectStoreNames); + + let objectStore3 = transaction.objectStore("foo"); + let objectStore4 = transaction.objectStore("foo"); + ok(objectStore3 === objectStore4, "Got same objectStores"); + + ok(objectStore3 !== objectStore1, "Different objectStores"); + ok(objectStore4 !== objectStore2, "Different objectStores"); + + let index3 = objectStore3.index("bar"); + let index4 = objectStore4.index("bar"); + ok(index3 === index4, "Got same indexes"); + + ok(index3 !== index1, "Different indexes"); + ok(index4 !== index2, "Different indexes"); + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_odd_result_order.js b/dom/indexedDB/test/unit/test_odd_result_order.js new file mode 100644 index 000000000..fb3d992c9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_odd_result_order.js @@ -0,0 +1,76 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const data = { key: 5, index: 10 }; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + ok(db instanceof IDBDatabase, "Got a real database"); + + db.onerror = errorHandler; + + let objectStore = db.createObjectStore("foo", { keyPath: "key", + autoIncrement: true }); + let index = objectStore.createIndex("foo", "index"); + + event.target.onsuccess = continueToNextStep; + yield undefined; + + objectStore = db.transaction("foo", "readwrite") + .objectStore("foo"); + request = objectStore.add(data); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let key; + executeSoon(function() { + key = request.result; + continueToNextStep(); + }); + yield undefined; + + is(key, data.key, "Got the right key"); + + objectStore = db.transaction("foo").objectStore("foo"); + objectStore.get(data.key).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let obj; + executeSoon(function() { + obj = event.target.result; + continueToNextStep(); + }); + yield undefined; + + is(obj.key, data.key, "Got the right key"); + is(obj.index, data.index, "Got the right property value"); + + objectStore = db.transaction("foo", "readwrite") + .objectStore("foo"); + request = objectStore.delete(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + key = undefined; + executeSoon(function() { + key = request.result; + continueToNextStep(); + }, 0); + yield undefined; + + ok(key === undefined, "Got the right value"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_oldDirectories.js b/dom/indexedDB/test/unit/test_oldDirectories.js new file mode 100644 index 000000000..47a24671e --- /dev/null +++ b/dom/indexedDB/test/unit/test_oldDirectories.js @@ -0,0 +1,72 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + // This lives in storage/default/http+++www.mozilla.org + const url = "http://www.mozilla.org"; + const dbName = "dbC"; + const dbVersion = 1; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase() { + let uri = ios.newURI(url, null, null); + let principal = ssm.createCodebasePrincipal(uri, {}); + let request = indexedDB.openForPrincipal(principal, dbName, dbVersion); + return request; + } + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("oldDirectories_profile"); + + let request = openDatabase(); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + request = openDatabase(); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Correct event type"); + + let directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + + let profileDir = directoryService.get("ProfD", Ci.nsIFile); + + let dir = profileDir.clone(); + dir.append("indexedDB"); + + let exists = dir.exists(); + ok(!exists, "indexedDB doesn't exist"); + + dir = profileDir.clone(); + dir.append("storage"); + dir.append("persistent"); + + exists = dir.exists(); + ok(!exists, "storage/persistent doesn't exist"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_open_empty_db.js b/dom/indexedDB/test/unit/test_open_empty_db.js new file mode 100644 index 000000000..add956669 --- /dev/null +++ b/dom/indexedDB/test/unit/test_open_empty_db.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const names = [ + //"", + null, + undefined, + this.window ? window.location.pathname : "Splendid Test" + ]; + + const version = 1; + + for (let name of names) { + let request = indexedDB.open(name, version); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + if (name === null) { + name = "null"; + } + else if (name === undefined) { + name = "undefined"; + } + + let db = event.target.result; + is(db.name, name, "Bad name"); + is(db.version, version, "Bad version"); + is(db.objectStoreNames.length, 0, "Bad objectStores list"); + + is(db.name, request.result.name, "Bad name"); + is(db.version, request.result.version, "Bad version"); + is(db.objectStoreNames.length, request.result.objectStoreNames.length, + "Bad objectStores list"); + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_open_for_principal.js b/dom/indexedDB/test/unit/test_open_for_principal.js new file mode 100644 index 000000000..171f71b39 --- /dev/null +++ b/dom/indexedDB/test/unit/test_open_for_principal.js @@ -0,0 +1,90 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + const objectStoreName = "Foo"; + + const data = { key: 1, value: "bar" }; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + request = objectStore.get(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, null, "Got no data"); + + request = objectStore.add(data.value, data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.key, "Got correct key"); + + let uri = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI("http://appdata.example.com", null, null); + let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager); + let principal = ssm.createCodebasePrincipal(uri, {}); + + request = indexedDB.openForPrincipal(principal, name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + db = event.target.result; + db.onerror = errorHandler; + + objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + objectStore = db.transaction([objectStoreName]) + .objectStore(objectStoreName); + + request = objectStore.get(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, null, "Got no data"); + + db.close(); + + request = indexedDB.deleteForPrincipal(principal, name); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler + event = yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_open_objectStore.js b/dom/indexedDB/test/unit/test_open_objectStore.js new file mode 100644 index 000000000..207107941 --- /dev/null +++ b/dom/indexedDB/test/unit/test_open_objectStore.js @@ -0,0 +1,39 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "Objects"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + is(db.objectStoreNames.length, 0, "Bad objectStores list"); + + let objectStore = db.createObjectStore(objectStoreName, + { keyPath: "foo" }); + + is(db.objectStoreNames.length, 1, "Bad objectStores list"); + is(db.objectStoreNames.item(0), objectStoreName, "Bad name"); + + yield undefined; + + objectStore = db.transaction(objectStoreName).objectStore(objectStoreName); + + is(objectStore.name, objectStoreName, "Bad name"); + is(objectStore.keyPath, "foo", "Bad keyPath"); + if(objectStore.indexNames.length, 0, "Bad indexNames"); + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_optionalArguments.js b/dom/indexedDB/test/unit/test_optionalArguments.js new file mode 100644 index 000000000..ad170dc8d --- /dev/null +++ b/dom/indexedDB/test/unit/test_optionalArguments.js @@ -0,0 +1,1711 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const osName = "people"; + const indexName = "weight"; + + const data = [ + { ssn: "237-23-7732", name: "Bob", height: 60, weight: 120 }, + { ssn: "237-23-7733", name: "Ann", height: 52, weight: 110 }, + { ssn: "237-23-7734", name: "Ron", height: 73, weight: 180 }, + { ssn: "237-23-7735", name: "Sue", height: 58, weight: 130 }, + { ssn: "237-23-7736", name: "Joe", height: 65, weight: 150 }, + { ssn: "237-23-7737", name: "Pat", height: 65 }, + { ssn: "237-23-7738", name: "Mel", height: 66, weight: {} }, + { ssn: "237-23-7739", name: "Tom", height: 62, weight: 130 } + ]; + + const weightSort = [1, 0, 3, 7, 4, 2]; + + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(osName, { keyPath: "ssn" }); + objectStore.createIndex(indexName, "weight", { unique: false }); + + for (let i of data) { + objectStore.add(i); + } + + event = yield undefined; + + is(event.type, "success", "Got success event"); + + try { + IDBKeyRange.bound(1, -1); + ok(false, "Bound keyRange with backwards args should throw!"); + } + catch (e) { + is(e.name, "DataError", "Threw correct exception"); + is(e.code, 0, "Threw with correct code"); + } + + try { + IDBKeyRange.bound(1, 1); + ok(true, "Bound keyRange with same arg should be ok"); + } + catch (e) { + ok(false, "Bound keyRange with same arg should have been ok"); + } + + try { + IDBKeyRange.bound(1, 1, true); + ok(false, "Bound keyRange with same arg and open should throw!"); + } + catch (e) { + is(e.name, "DataError", "Threw correct exception"); + is(e.code, 0, "Threw with correct code"); + } + + try { + IDBKeyRange.bound(1, 1, true, true); + ok(false, "Bound keyRange with same arg and open should throw!"); + } + catch (e) { + is(e.name, "DataError", "Threw correct exception"); + is(e.code, 0, "Threw with correct code"); + } + + objectStore = db.transaction(osName).objectStore(osName); + + try { + objectStore.get(); + ok(false, "Get with unspecified arg should have thrown"); + } + catch(e) { + ok(true, "Get with unspecified arg should have thrown"); + } + + try { + objectStore.get(undefined); + ok(false, "Get with undefined should have thrown"); + } + catch(e) { + ok(true, "Get with undefined arg should have thrown"); + } + + try { + objectStore.get(null); + ok(false, "Get with null should have thrown"); + } + catch(e) { + is(e instanceof DOMException, true, + "Got right kind of exception"); + is(e.name, "DataError", "Correct error."); + is(e.code, 0, "Correct code."); + } + + objectStore.get(data[2].ssn).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[2].name, "Correct data"); + + let keyRange = IDBKeyRange.only(data[2].ssn); + + objectStore.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[2].name, "Correct data"); + + keyRange = IDBKeyRange.lowerBound(data[2].ssn); + + objectStore.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[2].name, "Correct data"); + + keyRange = IDBKeyRange.lowerBound(data[2].ssn, true); + + objectStore.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[3].name, "Correct data"); + + keyRange = IDBKeyRange.upperBound(data[2].ssn); + + objectStore.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[0].name, "Correct data"); + + keyRange = IDBKeyRange.bound(data[2].ssn, data[4].ssn); + + objectStore.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[2].name, "Correct data"); + + keyRange = IDBKeyRange.bound(data[2].ssn, data[4].ssn, true); + + objectStore.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.name, data[3].name, "Correct data"); + + objectStore = db.transaction(osName, "readwrite") + .objectStore(osName); + + try { + objectStore.delete(); + ok(false, "Delete with unspecified arg should have thrown"); + } + catch(e) { + ok(true, "Delete with unspecified arg should have thrown"); + } + + try { + objectStore.delete(undefined); + ok(false, "Delete with undefined should have thrown"); + } + catch(e) { + ok(true, "Delete with undefined arg should have thrown"); + } + + try { + objectStore.delete(null); + ok(false, "Delete with null should have thrown"); + } + catch(e) { + is(e instanceof DOMException, true, + "Got right kind of exception"); + is(e.name, "DataError", "Correct error."); + is(e.code, 0, "Correct code."); + } + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.length, "Correct count"); + + objectStore.delete(data[2].ssn).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.result === undefined, "Correct result"); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.length - 1, "Correct count"); + + keyRange = IDBKeyRange.bound(data[3].ssn, data[5].ssn); + + objectStore.delete(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.result === undefined, "Correct result"); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.length - 4, "Correct count"); + + keyRange = IDBKeyRange.lowerBound(10); + + objectStore.delete(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + ok(event.target.result === undefined, "Correct result"); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, 0, "Correct count"); + + event.target.transaction.oncomplete = grabEventAndContinueHandler; + + for (let i of data) { + objectStore.add(i); + } + + yield undefined; + + objectStore = db.transaction(osName).objectStore(osName); + + objectStore.count().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.length, "Correct count"); + + let count = 0; + + objectStore.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, data.length, "Correct count for no arg to openCursor"); + + count = 0; + + objectStore.openCursor(null).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, data.length, "Correct count for null arg to openCursor"); + + count = 0; + + objectStore.openCursor(undefined).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, data.length, "Correct count for undefined arg to openCursor"); + + count = 0; + + objectStore.openCursor(data[2].ssn).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, "Correct count for single key arg to openCursor"); + + count = 0; + + objectStore.openCursor("foo").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent single key arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.only(data[2].ssn); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, "Correct count for only keyRange arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound(data[2].ssn); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, data.length - 2, + "Correct count for lowerBound arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound(data[2].ssn, true); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, data.length - 3, + "Correct count for lowerBound arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound("foo"); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent lowerBound arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[2].ssn, data[3].ssn); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 2, "Correct count for bound arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[2].ssn, data[3].ssn, true); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, "Correct count for bound arg to openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[2].ssn, data[3].ssn, true, true); + + objectStore.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, "Correct count for bound arg to openCursor"); + + let index = objectStore.index(indexName); + + count = 0; + + index.openKeyCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for unspecified arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor(null).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for null arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor(undefined).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for undefined arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor(data[0].weight).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, "Correct count for single key arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor("foo").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent key arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.only("foo"); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.only(data[0].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, + "Correct count for only keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for lowerBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight, true); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 1, + "Correct count for lowerBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound("foo"); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for lowerBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[0]].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, + "Correct count for upperBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[0]].weight, true); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for upperBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[weightSort.length - 1]].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for upperBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[weightSort.length - 1]].weight, + true); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 1, + "Correct count for upperBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound("foo"); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for upperBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(0); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for upperBound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[weightSort.length - 1]].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for bound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[weightSort.length - 1]].weight, + true); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 1, + "Correct count for bound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[weightSort.length - 1]].weight, + true, true); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 2, + "Correct count for bound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight - 1, + data[weightSort[weightSort.length - 1]].weight + 1); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for bound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight - 2, + data[weightSort[0]].weight - 1); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for bound keyRange arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[1]].weight, + data[weightSort[2]].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 3, + "Correct count for bound keyRange arg to index.openKeyCursor"); + + count = 0; + + index.openCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for unspecified arg to index.openCursor"); + + count = 0; + + index.openCursor(null).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for null arg to index.openCursor"); + + count = 0; + + index.openCursor(undefined).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for undefined arg to index.openCursor"); + + count = 0; + + index.openCursor(data[0].weight).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, "Correct count for single key arg to index.openCursor"); + + count = 0; + + index.openCursor("foo").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent key arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.only("foo"); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.only(data[0].weight); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, + "Correct count for only keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for lowerBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight, true); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 1, + "Correct count for lowerBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.lowerBound("foo"); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for lowerBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[0]].weight); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, + "Correct count for upperBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[0]].weight, true); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for upperBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[weightSort.length - 1]].weight); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for upperBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(data[weightSort[weightSort.length - 1]].weight, + true); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 1, + "Correct count for upperBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound("foo"); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for upperBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.upperBound(0); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for upperBound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[weightSort.length - 1]].weight); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for bound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[weightSort.length - 1]].weight, + true); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 1, + "Correct count for bound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[weightSort.length - 1]].weight, + true, true); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length - 2, + "Correct count for bound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight - 1, + data[weightSort[weightSort.length - 1]].weight + 1); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for bound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight - 2, + data[weightSort[0]].weight - 1); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for bound keyRange arg to index.openCursor"); + + count = 0; + keyRange = IDBKeyRange.bound(data[weightSort[1]].weight, + data[weightSort[2]].weight); + + index.openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 3, + "Correct count for bound keyRange arg to index.openCursor"); + + try { + index.get(); + ok(false, "Get with unspecified arg should have thrown"); + } + catch(e) { + ok(true, "Get with unspecified arg should have thrown"); + } + + try { + index.get(undefined); + ok(false, "Get with undefined should have thrown"); + } + catch(e) { + ok(true, "Get with undefined arg should have thrown"); + } + + try { + index.get(null); + ok(false, "Get with null should have thrown"); + } + catch(e) { + is(e instanceof DOMException, true, + "Got right kind of exception"); + is(e.name, "DataError", "Correct error."); + is(e.code, 0, "Correct code."); + } + + index.get(data[0].weight).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[0].weight, "Got correct result"); + + keyRange = IDBKeyRange.only(data[0].weight); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[0].weight, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[0]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight - 1); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[0]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight + 1); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[1]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight, true); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[1]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[1]].weight); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[0]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[1]].weight, true); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[1]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[1]].weight, true, true); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Got correct result"); + + keyRange = IDBKeyRange.upperBound(data[weightSort[5]].weight); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.weight, data[weightSort[0]].weight, + "Got correct result"); + + keyRange = IDBKeyRange.upperBound(data[weightSort[0]].weight, true); + + index.get(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Got correct result"); + + try { + index.getKey(); + ok(false, "Get with unspecified arg should have thrown"); + } + catch(e) { + ok(true, "Get with unspecified arg should have thrown"); + } + + try { + index.getKey(undefined); + ok(false, "Get with undefined should have thrown"); + } + catch(e) { + ok(true, "Get with undefined arg should have thrown"); + } + + try { + index.getKey(null); + ok(false, "Get with null should have thrown"); + } + catch(e) { + is(e instanceof DOMException, true, + "Got right kind of exception"); + is(e.name, "DataError", "Correct error."); + is(e.code, 0, "Correct code."); + } + + index.getKey(data[0].weight).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[0].ssn, "Got correct result"); + + keyRange = IDBKeyRange.only(data[0].weight); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[0].ssn, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[0]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight - 1); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[0]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight + 1); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[1]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(data[weightSort[0]].weight, true); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[1]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[1]].weight); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[0]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[1]].weight, true); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[1]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.bound(data[weightSort[0]].weight, + data[weightSort[1]].weight, true, true); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Got correct result"); + + keyRange = IDBKeyRange.upperBound(data[weightSort[5]].weight); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data[weightSort[0]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.upperBound(data[weightSort[0]].weight, true); + + index.getKey(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Got correct result"); + + count = 0; + + index.openKeyCursor().onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for no arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor(null).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for null arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor(undefined).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, weightSort.length, + "Correct count for undefined arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor(data[weightSort[0]].weight).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, "Correct count for single key arg to index.openKeyCursor"); + + count = 0; + + index.openKeyCursor("foo").onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 0, + "Correct count for non-existent single key arg to index.openKeyCursor"); + + count = 0; + keyRange = IDBKeyRange.only(data[weightSort[0]].weight); + + index.openKeyCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + count++; + cursor.continue(); + } + else { + testGenerator.next(); + } + } + yield undefined; + + is(count, 1, + "Correct count for only keyRange arg to index.openKeyCursor"); + + objectStore.mozGetAll(data[1].ssn).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, 1, "Got correct length"); + is(event.target.result[0].ssn, data[1].ssn, "Got correct result"); + + objectStore.mozGetAll(null).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, data.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[i].ssn, "Got correct value"); + } + + objectStore.mozGetAll(undefined).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, data.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[i].ssn, "Got correct value"); + } + + objectStore.mozGetAll().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, data.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[i].ssn, "Got correct value"); + } + + keyRange = IDBKeyRange.lowerBound(0); + + objectStore.mozGetAll(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, data.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[i].ssn, "Got correct value"); + } + + index.mozGetAll().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAll(undefined).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAll(null).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAll(data[weightSort[0]].weight).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, 1, "Got correct length"); + is(event.target.result[0].ssn, data[weightSort[0]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(0); + + index.mozGetAll(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i].ssn, data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i], data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAllKeys(undefined).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i], data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAllKeys(null).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i], data[weightSort[i]].ssn, + "Got correct value"); + } + + index.mozGetAllKeys(data[weightSort[0]].weight).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, 1, "Got correct length"); + is(event.target.result[0], data[weightSort[0]].ssn, "Got correct result"); + + keyRange = IDBKeyRange.lowerBound(0); + + index.mozGetAllKeys(keyRange).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result instanceof Array, true, "Got an array"); + is(event.target.result.length, weightSort.length, "Got correct length"); + for (let i in event.target.result) { + is(event.target.result[i], data[weightSort[i]].ssn, + "Got correct value"); + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_overlapping_transactions.js b/dom/indexedDB/test/unit/test_overlapping_transactions.js new file mode 100644 index 000000000..2606d7aea --- /dev/null +++ b/dom/indexedDB/test/unit/test_overlapping_transactions.js @@ -0,0 +1,92 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStores = [ "foo", "bar" ]; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + event.target.onsuccess = grabEventAndContinueHandler; + for (let i in objectStores) { + db.createObjectStore(objectStores[i], { autoIncrement: true }); + } + event = yield undefined; + + is(db.objectStoreNames.length, objectStores.length, + "Correct objectStoreNames list"); + + for (let i = 0; i < 50; i++) { + let stepNumber = 0; + + request = db.transaction(["foo"], "readwrite") + .objectStore("foo") + .add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(stepNumber, 1, "This callback came first"); + stepNumber++; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + } + + request = db.transaction(["foo"], "readwrite") + .objectStore("foo") + .add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(stepNumber, 2, "This callback came second"); + stepNumber++; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + } + + request = db.transaction(["foo", "bar"], "readwrite") + .objectStore("bar") + .add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(stepNumber, 3, "This callback came third"); + stepNumber++; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + } + + request = db.transaction(["foo", "bar"], "readwrite") + .objectStore("bar") + .add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(stepNumber, 4, "This callback came fourth"); + stepNumber++; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + } + + request = db.transaction(["bar"], "readwrite") + .objectStore("bar") + .add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(stepNumber, 5, "This callback came fifth"); + stepNumber++; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + } + + stepNumber++; + yield undefined; yield undefined; yield undefined; yield undefined; yield undefined; + + is(stepNumber, 6, "All callbacks received"); + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_persistenceType.js b/dom/indexedDB/test/unit/test_persistenceType.js new file mode 100644 index 000000000..0f5dd72a2 --- /dev/null +++ b/dom/indexedDB/test/unit/test_persistenceType.js @@ -0,0 +1,86 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = "Splendid Test"; + const version = 1; + + const objectStoreName = "Foo"; + const data = { key: 1, value: "bar" }; + + try { + indexedDB.open(name, { version: version, storage: "unknown" }); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof TypeError, "Got TypeError."); + is(e.name, "TypeError", "Good error name."); + } + + let request = indexedDB.open(name, { version: version, + storage: "persistent" }); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got correct event type"); + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + is(db.name, name, "Correct name"); + is(db.version, version, "Correct version"); + is(db.storage, "persistent", "Correct persistence type"); + + objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + request = objectStore.get(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Got no data"); + + request = objectStore.add(data.value, data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.key, "Got correct key"); + + request = indexedDB.open(name, { version: version, + storage: "temporary" }); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + is(db.name, name, "Correct name"); + is(db.version, version, "Correct version"); + is(db.storage, "persistent", "Correct persistence type"); + + objectStore = db.transaction([objectStoreName]) + .objectStore(objectStoreName); + + request = objectStore.get(data.key); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, data.value, "Got correct data"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_put_get_values.js b/dom/indexedDB/test/unit/test_put_get_values.js new file mode 100644 index 000000000..b7345795c --- /dev/null +++ b/dom/indexedDB/test/unit/test_put_get_values.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "Objects"; + + let testString = { key: 0, value: "testString" }; + let testInt = { key: 1, value: 1002 }; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, + { autoIncrement: 0 }); + + request = objectStore.add(testString.value, testString.key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, testString.key, "Got the right key"); + request = objectStore.get(testString.key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, testString.value, "Got the right value"); + }; + }; + + request = objectStore.add(testInt.value, testInt.key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, testInt.key, "Got the right key"); + request = objectStore.get(testInt.key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, testInt.value, "Got the right value"); + }; + } + + // Wait for success + yield undefined; + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_put_get_values_autoIncrement.js b/dom/indexedDB/test/unit/test_put_get_values_autoIncrement.js new file mode 100644 index 000000000..3291148d3 --- /dev/null +++ b/dom/indexedDB/test/unit/test_put_get_values_autoIncrement.js @@ -0,0 +1,54 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "Objects"; + + let testString = { value: "testString" }; + let testInt = { value: 1002 }; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore(objectStoreName, + { autoIncrement: 1 }); + + request = objectStore.put(testString.value); + request.onerror = errorHandler; + request.onsuccess = function(event) { + testString.key = event.target.result; + request = objectStore.get(testString.key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, testString.value, "Got the right value"); + }; + }; + + request = objectStore.put(testInt.value); + request.onerror = errorHandler; + request.onsuccess = function(event) { + testInt.key = event.target.result; + request = objectStore.get(testInt.key); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, testInt.value, "Got the right value"); + }; + } + + // Wait for success + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_quotaExceeded_recovery.js b/dom/indexedDB/test/unit/test_quotaExceeded_recovery.js new file mode 100644 index 000000000..1f04bb6d0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_quotaExceeded_recovery.js @@ -0,0 +1,141 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need a way to set temporary prefs from a worker"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const spec = "http://foo.com"; + const name = + this.window ? window.location.pathname : "test_quotaExceeded_recovery"; + const objectStoreName = "foo"; + + const android = mozinfo.os == "android"; + + // We want 512 KB database on Android and 4 MB database on other platforms. + const groupLimitKB = android ? 512 : 4096; + + // The group limit is calculated as 20% of the global temporary storage limit. + const tempStorageLimitKB = groupLimitKB * 5; + + // We want 64 KB chunks on Android and 512 KB chunks on other platforms. + const dataSizeKB = android ? 64 : 512; + const dataSize = dataSizeKB * 1024; + + const maxIter = 5; + + for (let blobs of [false, true]) { + setTemporaryStorageLimit(tempStorageLimitKB); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + info("Opening database"); + + let request = indexedDB.openForPrincipal(getPrincipal(spec), name); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler;; + request.onsuccess = unexpectedSuccessHandler; + + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName, { autoIncrement: true }); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + ok(true, "Filling database"); + + let obj = { + name: "foo" + } + + if (!blobs) { + obj.data = getRandomView(dataSize); + } + + let iter = 1; + let i = 1; + let j = 1; + while (true) { + if (blobs) { + obj.data = getBlob(getView(dataSize)); + } + + let trans = db.transaction(objectStoreName, "readwrite"); + request = trans.objectStore(objectStoreName).add(obj); + request.onerror = function(event) + { + event.stopPropagation(); + } + + trans.oncomplete = function(event) { + if (iter == 1) { + i++; + } + j++; + testGenerator.send(true); + } + trans.onabort = function(event) { + is(trans.error.name, "QuotaExceededError", "Reached quota limit"); + testGenerator.send(false); + } + + let completeFired = yield undefined; + if (completeFired) { + ok(true, "Got complete event"); + continue; + } + + ok(true, "Got abort event"); + + if (iter++ == maxIter) { + break; + } + + if (iter > 1) { + ok(i == j, "Recycled entire database"); + j = 1; + } + + trans = db.transaction(objectStoreName, "readwrite"); + + // Don't use a cursor for deleting stored blobs (Cursors prolong live + // of stored files since each record must be fetched from the database + // first which creates a memory reference to the stored blob.) + if (blobs) { + request = trans.objectStore(objectStoreName).clear(); + } else { + request = trans.objectStore(objectStoreName).openCursor(); + request.onsuccess = function(event) { + let cursor = event.target.result; + if (cursor) { + cursor.delete(); + cursor.continue(); + } + } + } + + trans.onabort = unexpectedSuccessHandler;; + trans.oncomplete = grabEventAndContinueHandler; + + yield undefined; + } + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_readonly_transactions.js b/dom/indexedDB/test/unit/test_readonly_transactions.js new file mode 100644 index 000000000..91f0f4c9a --- /dev/null +++ b/dom/indexedDB/test/unit/test_readonly_transactions.js @@ -0,0 +1,174 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const osName = "foo"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + db.createObjectStore(osName, { autoIncrement: "true" }); + + yield undefined; + + let key1, key2; + + request = db.transaction([osName], "readwrite") + .objectStore(osName) + .add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + key1 = event.target.result; + testGenerator.next(); + } + yield undefined; + + request = db.transaction(osName, "readwrite").objectStore(osName).add({}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + key2 = event.target.result; + testGenerator.next(); + } + yield undefined; + + request = db.transaction([osName], "readwrite") + .objectStore(osName) + .put({}, key1); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + testGenerator.next(); + } + yield undefined; + + request = db.transaction(osName, "readwrite") + .objectStore(osName) + .put({}, key2); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + testGenerator.next(); + } + yield undefined; + + request = db.transaction([osName], "readwrite") + .objectStore(osName) + .put({}, key1); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + testGenerator.next(); + } + yield undefined; + + request = db.transaction(osName, "readwrite") + .objectStore(osName) + .put({}, key1); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + testGenerator.next(); + } + yield undefined; + + request = db.transaction([osName], "readwrite") + .objectStore(osName) + .delete(key1); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + testGenerator.next(); + } + yield undefined; + + request = db.transaction(osName, "readwrite") + .objectStore(osName) + .delete(key2); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readwrite", "Correct mode"); + testGenerator.next(); + } + yield undefined; + + try { + request = db.transaction([osName]).objectStore(osName).add({}); + ok(false, "Adding to a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Adding to a readonly transaction failed"); + } + + try { + request = db.transaction(osName).objectStore(osName).add({}); + ok(false, "Adding to a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Adding to a readonly transaction failed"); + } + + try { + request = db.transaction([osName]).objectStore(osName).put({}); + ok(false, "Adding or modifying a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Adding or modifying a readonly transaction failed"); + } + + try { + request = db.transaction(osName).objectStore(osName).put({}); + ok(false, "Adding or modifying a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Adding or modifying a readonly transaction failed"); + } + + try { + request = db.transaction([osName]).objectStore(osName).put({}, key1); + ok(false, "Modifying a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Modifying a readonly transaction failed"); + } + + try { + request = db.transaction(osName).objectStore(osName).put({}, key1); + ok(false, "Modifying a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Modifying a readonly transaction failed"); + } + + try { + request = db.transaction([osName]).objectStore(osName).delete(key1); + ok(false, "Removing from a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Removing from a readonly transaction failed"); + } + + try { + request = db.transaction(osName).objectStore(osName).delete(key2); + ok(false, "Removing from a readonly transaction should fail!"); + } + catch (e) { + ok(true, "Removing from a readonly transaction failed"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_readwriteflush_disabled.js b/dom/indexedDB/test/unit/test_readwriteflush_disabled.js new file mode 100644 index 000000000..484b1aa42 --- /dev/null +++ b/dom/indexedDB/test/unit/test_readwriteflush_disabled.js @@ -0,0 +1,72 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need a way to set temporary prefs from a worker"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_readwriteflush_disabled.js"; + + info("Resetting experimental pref"); + + if (this.window) { + SpecialPowers.pushPrefEnv( + { + "set": [ + ["dom.indexedDB.experimental", false] + ] + }, + continueToNextStep + ); + yield undefined; + } else { + resetExperimental(); + } + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + request.result.createObjectStore(name); + + yield undefined; + + // success + let db = request.result; + + info("Attempting to create a 'readwriteflush' transaction"); + + let exception; + + try { + let transaction = db.transaction(name, "readwriteflush"); + } catch (e) { + exception = e; + } + + ok(exception, "'readwriteflush' transaction threw"); + ok(exception instanceof Error, "exception is an Error object"); + is(exception.message, + "Argument 2 of IDBDatabase.transaction 'readwriteflush' is not a valid " + + "value for enumeration IDBTransactionMode.", + "exception has the correct message"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_remove_index.js b/dom/indexedDB/test/unit/test_remove_index.js new file mode 100644 index 000000000..d9916b975 --- /dev/null +++ b/dom/indexedDB/test/unit/test_remove_index.js @@ -0,0 +1,58 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const indexName = "My Test Index"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore = db.createObjectStore("test store", { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStore.name, "Correct name"); + + is(objectStore.indexNames.length, 0, "Correct indexNames list"); + + let index = objectStore.createIndex(indexName, "foo"); + + is(objectStore.indexNames.length, 1, "Correct indexNames list"); + is(objectStore.indexNames.item(0), indexName, "Correct name"); + is(objectStore.index(indexName), index, "Correct instance"); + + objectStore.deleteIndex(indexName); + + is(objectStore.indexNames.length, 0, "Correct indexNames list"); + try { + objectStore.index(indexName); + ok(false, "should have thrown"); + } + catch(ex) { + ok(ex instanceof DOMException, "Got a DOMException"); + is(ex.name, "NotFoundError", "expect a NotFoundError"); + is(ex.code, DOMException.NOT_FOUND_ERR, "expect a NOT_FOUND_ERR"); + } + + let index2 = objectStore.createIndex(indexName, "foo"); + isnot(index, index2, "New instance should be created"); + + is(objectStore.indexNames.length, 1, "Correct recreacted indexNames list"); + is(objectStore.indexNames.item(0), indexName, "Correct recreacted name"); + is(objectStore.index(indexName), index2, "Correct instance"); + + event.target.transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_remove_objectStore.js b/dom/indexedDB/test/unit/test_remove_objectStore.js new file mode 100644 index 000000000..324fa0d2f --- /dev/null +++ b/dom/indexedDB/test/unit/test_remove_objectStore.js @@ -0,0 +1,129 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "Objects"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + let db = event.target.result; + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore = db.createObjectStore(objectStoreName, + { keyPath: "foo" }); + + let addedCount = 0; + + for (let i = 0; i < 100; i++) { + request = objectStore.add({foo: i}); + request.onerror = errorHandler; + request.onsuccess = function(event) { + if (++addedCount == 100) { + executeSoon(function() { testGenerator.next(); }); + } + } + } + yield undefined; + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStoreName, "Correct name"); + + event.target.transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + // Wait for success. + event = yield undefined; + + db.close(); + + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + db = event.target.result; + let trans = event.target.transaction; + + let oldObjectStore = trans.objectStore(objectStoreName); + isnot(oldObjectStore, null, "Correct object store prior to deleting"); + db.deleteObjectStore(objectStoreName); + is(db.objectStoreNames.length, 0, "Correct objectStores list"); + try { + trans.objectStore(objectStoreName); + ok(false, "should have thrown"); + } + catch(ex) { + ok(ex instanceof DOMException, "Got a DOMException"); + is(ex.name, "NotFoundError", "expect a NotFoundError"); + is(ex.code, DOMException.NOT_FOUND_ERR, "expect a NOT_FOUND_ERR"); + } + + objectStore = db.createObjectStore(objectStoreName, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStoreName, "Correct name"); + is(trans.objectStore(objectStoreName), objectStore, "Correct new objectStore"); + isnot(oldObjectStore, objectStore, "Old objectStore is not new objectStore"); + + request = objectStore.openCursor(); + request.onerror = errorHandler; + request.onsuccess = function(event) { + is(event.target.result, null, "ObjectStore shouldn't have any items"); + testGenerator.send(event); + } + event = yield undefined; + + db.deleteObjectStore(objectStore.name); + is(db.objectStoreNames.length, 0, "Correct objectStores list"); + + continueToNextStep(); + yield undefined; + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + // Wait for success. + event = yield undefined; + + db.close(); + + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + trans = event.target.transaction; + + objectStore = db.createObjectStore(objectStoreName, { keyPath: "foo" }); + + request = objectStore.add({foo:"bar"}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + db.deleteObjectStore(objectStoreName); + + event = yield undefined; + + trans.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_rename_index.js b/dom/indexedDB/test/unit/test_rename_index.js new file mode 100644 index 000000000..e7ab8c038 --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_index.js @@ -0,0 +1,193 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const storeName = "test store"; + const indexName_ToBeDeleted = "test index to be deleted"; + const indexName_v0 = "test index v0"; + const indexName_v1 = "test index v1"; + const indexName_v2 = "test index v2"; + const indexName_v3 = indexName_ToBeDeleted; + const indexName_v4 = "test index v4"; + + info("Rename in v1."); + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore = db.createObjectStore(storeName, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStore.name, "Correct object store name"); + + // create index to be deleted later in v3. + objectStore.createIndex(indexName_ToBeDeleted, "foo"); + ok(objectStore.index(indexName_ToBeDeleted), "Index created."); + + // create target index to be renamed. + let index = objectStore.createIndex(indexName_v0, "bar"); + ok(objectStore.index(indexName_v0), "Index created."); + is(index.name, indexName_v0, "Correct index name"); + index.name = indexName_v1; + is(index.name, indexName_v1, "Renamed index successfully"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify renaming done in v1 and run renaming in v2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + objectStore = txn.objectStore(storeName); + + // indexName_v0 created in v1 shall not be available. + try { + index = objectStore.index(indexName_v0); + ok(false, "NotFoundError shall be thrown."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "NotFoundError", "correct error"); + } + + // rename to "v2". + index = objectStore.index(indexName_v1); + is(index.name, indexName_v1, "Correct index name") + index.name = indexName_v2; + is(index.name, indexName_v2, "Renamed index successfully"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify renaming done in v2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + txn = db.transaction(storeName); + + objectStore = txn.objectStore(storeName); + index = objectStore.index(indexName_v2); + is(index.name, indexName_v2, "Correct index name"); + + db.close(); + + info("Rename in v3."); + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + objectStore = txn.objectStore(storeName); + ok(objectStore.index(indexName_ToBeDeleted), "index is valid."); + objectStore.deleteIndex(indexName_ToBeDeleted); + try { + objectStore.index(indexName_ToBeDeleted); + ok(false, "NotFoundError shall be thrown if the index name is deleted."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "NotFoundError", "correct error"); + } + + info("Rename with the name of the deleted index."); + index = objectStore.index(indexName_v2); + index.name = indexName_v3; + is(index.name, indexName_v3, "Renamed index successfully"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify renaming done in v3."); + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + txn = db.transaction(storeName); + + objectStore = txn.objectStore(storeName); + index = objectStore.index(indexName_v3); + is(index.name, indexName_v3, "Correct index name"); + + db.close(); + + info("Abort the version change transaction while renaming index."); + request = indexedDB.open(name, 4); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + objectStore = txn.objectStore(storeName); + index = objectStore.index(indexName_v3); + index.name = indexName_v4; + is(index.name, indexName_v4, "Renamed successfully"); + let putRequest = objectStore.put({ foo: "fooValue", bar: "barValue" }); + putRequest.onsuccess = continueToNextStepSync; + yield undefined; + + // Aborting the transaction. + request.onerror = expectedErrorHandler("AbortError"); + txn.abort(); + yield undefined; + + // Verify if the name of the index handle is reverted. + is(index.name, indexName_v3, "The name is reverted after aborted."); + + info("Verify if the objectstore name is unchanged."); + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + txn = db.transaction(storeName); + + objectStore = txn.objectStore(storeName); + index = objectStore.index(indexName_v3); + is(index.name, indexName_v3, "Correct index name"); + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_rename_index_errors.js b/dom/indexedDB/test/unit/test_rename_index_errors.js new file mode 100644 index 000000000..cac972a1f --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_index_errors.js @@ -0,0 +1,129 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const storeName = "test store"; + const indexName1 = "test index 1"; + const indexName2 = "test index 2"; + + info("Setup test indexes."); + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore = db.createObjectStore(storeName, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStore.name, "Correct name"); + + let index1 = objectStore.createIndex(indexName1, "bar"); + is(objectStore.index(indexName1).name, index1.name, "Correct index name"); + is(index1.name, indexName1, "Correct index name"); + let index2 = objectStore.createIndex(indexName2, "baz"); + is(objectStore.index(indexName2).name, index2.name, "Correct index name"); + is(index2.name, indexName2, "Correct index name"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify IDB Errors in version 2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + objectStore = txn.objectStore(storeName); + index1 = objectStore.index(indexName1); + index2 = objectStore.index(indexName2); + is(index1.name, indexName1, "Correct index name"); + is(index2.name, indexName2, "Correct index name"); + + // Rename with the name already adopted by the other index. + try { + index1.name = indexName2; + ok(false, "ConstraintError shall be thrown if the index name already exists."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "ConstraintError", "correct error"); + } + + // Rename with identical name. + try { + index1.name = indexName1; + ok(true, "It shall be fine to set the same name."); + } catch (e) { + ok(false, "Got a database exception: " + e.name); + } + + objectStore.deleteIndex(indexName2); + + // Rename after deleted. + try { + index2.name = indexName2; + ok(false, "InvalidStateError shall be thrown if deleted."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + } + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + // Rename when the transaction is inactive. + try { + index1.name = indexName1; + ok(false, "TransactionInactiveError shall be thrown if the transaction is inactive."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TransactionInactiveError", "correct error"); + } + + info("Rename when the transaction is not an upgrade one."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + txn = db.transaction(storeName); + objectStore = txn.objectStore(storeName); + index1 = objectStore.index(indexName1); + + try { + index1.name = indexName1; + ok(false, "InvalidStateError shall be thrown if it's not an upgrade transaction."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + } + + txn.oncomplete = continueToNextStepSync; + yield undefined; + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_rename_objectStore.js b/dom/indexedDB/test/unit/test_rename_objectStore.js new file mode 100644 index 000000000..4c7796889 --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_objectStore.js @@ -0,0 +1,171 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const storeName_ToBeDeleted = "test store to be deleted"; + const storeName_v0 = "test store v0"; + const storeName_v1 = "test store v1"; + const storeName_v2 = "test store v2"; + const storeName_v3 = storeName_ToBeDeleted; + const storeName_v4 = "test store v4"; + + info("Rename in v1."); + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + // create objectstore to be deleted later in v3. + db.createObjectStore(storeName_ToBeDeleted, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_ToBeDeleted), "Correct name"); + + // create target objectstore to be renamed. + let objectStore = db.createObjectStore(storeName_v0, { keyPath: "bar" }); + is(db.objectStoreNames.length, 2, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(objectStore.name), "Correct name"); + + objectStore.name = storeName_v1; + is(objectStore.name, storeName_v1, "Renamed successfully"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify renaming done in v1 and run renaming in v2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + is(db.objectStoreNames.length, 2, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_v1), "Correct name"); + ok(db.objectStoreNames.contains(storeName_ToBeDeleted), "Correct name"); + + objectStore = txn.objectStore(storeName_v1); + objectStore.name = storeName_v2; + is(objectStore.name, storeName_v2, "Renamed successfully"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify renaming done in v2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + + is(db.objectStoreNames.length, 2, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_v2), "Correct name"); + ok(db.objectStoreNames.contains(storeName_ToBeDeleted), "Correct name"); + + db.close(); + + info("Rename in v3."); + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + is(db.objectStoreNames.length, 2, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_v2), "Correct name"); + ok(db.objectStoreNames.contains(storeName_ToBeDeleted), "Correct name"); + db.deleteObjectStore(storeName_ToBeDeleted); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_v2) && + !db.objectStoreNames.contains(storeName_ToBeDeleted), "Deleted correctly"); + + objectStore = txn.objectStore(storeName_v2); + objectStore.name = storeName_v3; + is(objectStore.name, storeName_v3, "Renamed successfully"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify renaming done in v3."); + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_v3), "Correct name"); + + db.close(); + + info("Abort the version change transaction while renaming objectstore."); + request = indexedDB.open(name, 4); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + objectStore = txn.objectStore(storeName_v3); + objectStore.name = storeName_v4; + is(objectStore.name, storeName_v4, "Renamed successfully"); + let putRequest = objectStore.put({ bar: "barValue" }); + putRequest.onsuccess = continueToNextStepSync; + yield undefined; + + // Aborting the transaction. + request.onerror = expectedErrorHandler("AbortError"); + txn.abort(); + yield undefined; + + // Verify if the name of the objectStore handle is reverted. + is(objectStore.name, storeName_v3, "The name is reverted after aborted."); + + info("Verify if the objectstore name is unchanged."); + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + ok(db.objectStoreNames.contains(storeName_v3), "Correct name"); + + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_rename_objectStore_errors.js b/dom/indexedDB/test/unit/test_rename_objectStore_errors.js new file mode 100644 index 000000000..0e4b796a6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_objectStore_errors.js @@ -0,0 +1,127 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + const storeName1 = "test store 1"; + const storeName2 = "test store 2"; + + info("Setup test object stores."); + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + let txn = event.target.transaction; + + is(db.objectStoreNames.length, 0, "Correct objectStoreNames list"); + + let objectStore1 = db.createObjectStore(storeName1, { keyPath: "foo" }); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(0), objectStore1.name, "Correct name"); + is(objectStore1.name, storeName1, "Correct name"); + + let objectStore2 = db.createObjectStore(storeName2, { keyPath: "bar" }); + is(db.objectStoreNames.length, 2, "Correct objectStoreNames list"); + is(db.objectStoreNames.item(1), objectStore2.name, "Correct name"); + is(objectStore2.name, storeName2, "Correct name"); + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Verify IDB Errors in version 2."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = continueToNextStep; + event = yield undefined; + + db = event.target.result; + txn = event.target.transaction; + + is(db.objectStoreNames.length, 2, "Correct objectStoreNames list"); + + objectStore1 = txn.objectStore(storeName1); + objectStore2 = txn.objectStore(storeName2); + is(objectStore1.name, storeName1, "Correct name"); + is(objectStore2.name, storeName2, "Correct name"); + + // Rename with the name already adopted by the other object store. + try { + objectStore1.name = storeName2; + ok(false, "ConstraintError shall be thrown if the store name already exists."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "ConstraintError", "correct error"); + } + + // Rename with the identical name. + try { + objectStore1.name = storeName1; + ok(true, "It shall be fine to set the same name."); + } catch (e) { + ok(false, "Got a database exception: " + e.name); + } + + db.deleteObjectStore(storeName2); + + // Rename after deleted. + try { + objectStore2.name = storeName2; + ok(false, "InvalidStateError shall be thrown if deleted."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + } + + txn.oncomplete = continueToNextStepSync; + yield undefined; + request.onsuccess = continueToNextStep; + yield undefined; + db.close(); + + info("Rename when the transaction is inactive."); + try { + objectStore1.name = storeName1; + ok(false, "TransactionInactiveError shall be thrown if the transaction is inactive."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TransactionInactiveError", "correct error"); + } + + info("Rename when the transaction is not an upgrade one."); + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db = event.target.result; + txn = db.transaction(storeName1); + objectStore1 = txn.objectStore(storeName1); + + try { + objectStore1.name = storeName1; + ok(false, "InvalidStateError shall be thrown if it's not an upgrade transaction."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "InvalidStateError", "correct error"); + } + + txn.oncomplete = continueToNextStepSync; + yield undefined; + db.close(); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_request_readyState.js b/dom/indexedDB/test/unit/test_request_readyState.js new file mode 100644 index 000000000..c19d974a3 --- /dev/null +++ b/dom/indexedDB/test/unit/test_request_readyState.js @@ -0,0 +1,51 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + is(request.readyState, "pending", "Correct readyState"); + + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(request.readyState, "done", "Correct readyState"); + + let db = event.target.result; + + let objectStore = db.createObjectStore("foo"); + let key = 10; + + request = objectStore.add({}, key); + is(request.readyState, "pending", "Correct readyState"); + + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(request.readyState, "done", "Correct readyState"); + is(event.target.result, key, "Correct key"); + + request = objectStore.get(key); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + is(request.readyState, "pending", "Correct readyState"); + event = yield undefined; + + ok(event.target.result, "Got something"); + is(request.readyState, "done", "Correct readyState"); + + // Wait for success + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_sandbox.js b/dom/indexedDB/test/unit/test_sandbox.js new file mode 100644 index 000000000..bcd4cad51 --- /dev/null +++ b/dom/indexedDB/test/unit/test_sandbox.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function exerciseInterface() { + function DB(name, store) { + this.name = name; + this.store = store; + this._db = this._create(); + } + + DB.prototype = { + _create: function() { + var op = indexedDB.open(this.name); + op.onupgradeneeded = e => { + var db = e.target.result; + db.createObjectStore(this.store); + }; + return new Promise(resolve => { + op.onsuccess = e => resolve(e.target.result); + }); + }, + + _result: function(tx, op) { + return new Promise((resolve, reject) => { + op.onsuccess = e => resolve(e.target.result); + op.onerror = () => reject(op.error); + tx.onabort = () => reject(tx.error); + }); + }, + + get: function(k) { + return this._db.then(db => { + var tx = db.transaction(this.store, 'readonly'); + var store = tx.objectStore(this.store); + return this._result(tx, store.get(k)); + }); + }, + + add: function(k, v) { + return this._db.then(db => { + var tx = db.transaction(this.store, 'readwrite'); + var store = tx.objectStore(this.store); + return this._result(tx, store.add(v, k)); + }); + } + }; + + var db = new DB('data', 'base'); + return db.add('x', [ 10, {} ]) + .then(_ => db.get('x')) + .then(x => { + equal(x.length, 2); + equal(x[0], 10); + equal(typeof x[1], 'object'); + equal(Object.keys(x[1]).length, 0); + }); +} + +function run_test() { + do_get_profile(); + + let Cu = Components.utils; + let sb = new Cu.Sandbox('https://www.example.com', + { wantGlobalProperties: ['indexedDB'] }); + + sb.equal = equal; + var innerPromise = new Promise((resolve, reject) => { + sb.test_done = resolve; + sb.test_error = reject; + }); + Cu.evalInSandbox('(' + exerciseInterface.toSource() + ')()' + + '.then(test_done, test_error);', sb); + + Cu.importGlobalProperties(['indexedDB']); + do_test_pending(); + Promise.all([innerPromise, exerciseInterface()]) + .then(do_test_finished); +} diff --git a/dom/indexedDB/test/unit/test_schema18upgrade.js b/dom/indexedDB/test/unit/test_schema18upgrade.js new file mode 100644 index 000000000..f2069018f --- /dev/null +++ b/dom/indexedDB/test/unit/test_schema18upgrade.js @@ -0,0 +1,336 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const testName = "schema18upgrade"; + const testKeys = [ + -1/0, + -1.7e308, + -10000, + -2, + -1.5, + -1, + -1.00001e-200, + -1e-200, + 0, + 1e-200, + 1.00001e-200, + 1, + 2, + 10000, + 1.7e308, + 1/0, + new Date("1750-01-02"), + new Date("1800-12-31T12:34:56.001Z"), + new Date(-1000), + new Date(-10), + new Date(-1), + new Date(0), + new Date(1), + new Date(2), + new Date(1000), + new Date("1971-01-01"), + new Date("1971-01-01T01:01:01Z"), + new Date("1971-01-01T01:01:01.001Z"), + new Date("1971-01-01T01:01:01.01Z"), + new Date("1971-01-01T01:01:01.1Z"), + new Date("1980-02-02"), + new Date("3333-03-19T03:33:33.333Z"), + "", + "\x00", + "\x00\x00", + "\x00\x01", + "\x01", + "\x02", + "\x03", + "\x04", + "\x07", + "\x08", + "\x0F", + "\x10", + "\x1F", + "\x20", + "01234", + "\x3F", + "\x40", + "A", + "A\x00", + "A1", + "ZZZZ", + "a", + "a\x00", + "aa", + "azz", + "}", + "\x7E", + "\x7F", + "\x80", + "\xFF", + "\u0100", + "\u01FF", + "\u0200", + "\u03FF", + "\u0400", + "\u07FF", + "\u0800", + "\u0FFF", + "\u1000", + "\u1FFF", + "\u2000", + "\u3FFF", + "\u4000", + "\u7FFF", + "\u8000", + "\uD800", + "\uD800a", + "\uD800\uDC01", + "\uDBFF", + "\uDC00", + "\uDFFF\uD800", + "\uFFFE", + "\uFFFF", + "\uFFFF\x00", + "\uFFFFZZZ", + [], + [-1/0], + [-1], + [0], + [1], + [1, "a"], + [1, []], + [1, [""]], + [2, 3], + [2, 3.0000000000001], + [12, [[]]], + [12, [[[]]]], + [12, [[[""]]]], + [12, [[["foo"]]]], + [12, [[[[[3]]]]]], + [12, [[[[[[3]]]]]]], + [12, [[[[[[3],[[[[[4.2]]]]]]]]]]], + [new Date(-1)], + [new Date(1)], + [""], + ["", [[]]], + ["", [[[]]]], + ["abc"], + ["abc", "def"], + ["abc\x00"], + ["abc\x00", "\x00\x01"], + ["abc\x00", "\x00def"], + ["abc\x00\x00def"], + ["x", [[]]], + ["x", [[[]]]], + [[]], + [[],"foo"], + [[],[]], + [[[]]], + [[[]], []], + [[[]], [[]]], + [[[]], [[1]]], + [[[]], [[[]]]], + [[[1]]], + [[[[]], []]], + ]; + const testString = + "abcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()-_+=,<.>/?\\|"; + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + info("Installing profile"); + + installPackagedProfile(testName + "_profile"); + + info("Opening database with no version"); + + let request = indexedDB.open(testName); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + is(db.version, 1, "Correct db version"); + + let transaction = db.transaction(testName); + transaction.oncomplete = grabEventAndContinueHandler; + + let objectStore = transaction.objectStore(testName); + let index = objectStore.index("uniqueIndex"); + + info("Starting 'uniqueIndex' cursor"); + + let keyIndex = 0; + index.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + info("Comparing " + JSON.stringify(cursor.primaryKey) + " to " + + JSON.stringify(testKeys[cursor.key]) + + " [" + cursor.key + "]"); + is(indexedDB.cmp(cursor.primaryKey, testKeys[cursor.key]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(cursor.primaryKey, testKeys[cursor.key]), true, + "Keys compare equally via 'compareKeys'"); + + let indexProperty = cursor.value.index; + is(Array.isArray(indexProperty), true, "index property is Array"); + is(indexProperty[0], cursor.key, "index property first item correct"); + is(indexProperty[1], cursor.key + 1, "index property second item correct"); + + is(cursor.key, keyIndex, "Cursor key property is correct"); + + is(cursor.value.testString, testString, "Test string compared equally"); + + keyIndex++; + cursor.continue(); + } + }; + yield undefined; + + is(keyIndex, testKeys.length, "Saw all keys"); + + transaction = db.transaction(testName, "readwrite"); + transaction.oncomplete = grabEventAndContinueHandler; + + objectStore = transaction.objectStore(testName); + index = objectStore.index("index"); + + info("Getting all 'index' keys"); + + index.getAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.length, testKeys.length * 2, "Got all keys"); + + info("Starting objectStore cursor"); + + objectStore.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + let value = cursor.value; + is(value.testString, testString, "Test string compared equally"); + + delete value.index; + cursor.update(value); + + cursor.continue(); + } else { + continueToNextStepSync(); + } + }; + yield undefined; + + info("Getting all 'index' keys"); + + index.getAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.length, 0, "Removed all keys"); + yield undefined; + + db.close(); + + info("Opening database with new version"); + + request = indexedDB.open(testName, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Deleting indexes"); + + objectStore = event.target.transaction.objectStore(testName); + objectStore.deleteIndex("index"); + objectStore.deleteIndex("uniqueIndex"); + + event = yield undefined; + + db = event.target.result; + + transaction = db.transaction(testName, "readwrite"); + transaction.oncomplete = grabEventAndContinueHandler; + + info("Starting objectStore cursor"); + + objectStore = transaction.objectStore(testName); + objectStore.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + let value = cursor.value; + is(value.testString, testString, "Test string compared equally"); + + value.index = value.keyPath; + cursor.update(value); + + cursor.continue(); + } + }; + event = yield undefined; + + db.close(); + + info("Opening database with new version"); + + request = indexedDB.open(testName, 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Creating indexes"); + + objectStore = event.target.transaction.objectStore(testName); + objectStore.createIndex("index", "index"); + + event = yield undefined; + + db = event.target.result; + + transaction = db.transaction(testName); + transaction.oncomplete = grabEventAndContinueHandler; + + objectStore = transaction.objectStore(testName); + index = objectStore.index("index"); + + info("Starting 'index' cursor"); + + keyIndex = 0; + index.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + is(indexedDB.cmp(cursor.primaryKey, testKeys[keyIndex]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(cursor.primaryKey, testKeys[keyIndex]), true, + "Keys compare equally via 'compareKeys'"); + is(indexedDB.cmp(cursor.key, testKeys[keyIndex]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(cursor.key, testKeys[keyIndex]), true, + "Keys compare equally via 'compareKeys'"); + + let indexProperty = cursor.value.index; + is(indexedDB.cmp(indexProperty, testKeys[keyIndex]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(indexProperty, testKeys[keyIndex]), true, + "Keys compare equally via 'compareKeys'"); + + is(cursor.value.testString, testString, "Test string compared equally"); + + keyIndex++; + cursor.continue(); + } + }; + yield undefined; + + is(keyIndex, testKeys.length, "Added all keys again"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_schema21upgrade.js b/dom/indexedDB/test/unit/test_schema21upgrade.js new file mode 100644 index 000000000..2cd6e45b2 --- /dev/null +++ b/dom/indexedDB/test/unit/test_schema21upgrade.js @@ -0,0 +1,336 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const testName = "schema21upgrade"; + const testKeys = [ + -1/0, + -1.7e308, + -10000, + -2, + -1.5, + -1, + -1.00001e-200, + -1e-200, + 0, + 1e-200, + 1.00001e-200, + 1, + 2, + 10000, + 1.7e308, + 1/0, + new Date("1750-01-02"), + new Date("1800-12-31T12:34:56.001Z"), + new Date(-1000), + new Date(-10), + new Date(-1), + new Date(0), + new Date(1), + new Date(2), + new Date(1000), + new Date("1971-01-01"), + new Date("1971-01-01T01:01:01Z"), + new Date("1971-01-01T01:01:01.001Z"), + new Date("1971-01-01T01:01:01.01Z"), + new Date("1971-01-01T01:01:01.1Z"), + new Date("1980-02-02"), + new Date("3333-03-19T03:33:33.333Z"), + "", + "\x00", + "\x00\x00", + "\x00\x01", + "\x01", + "\x02", + "\x03", + "\x04", + "\x07", + "\x08", + "\x0F", + "\x10", + "\x1F", + "\x20", + "01234", + "\x3F", + "\x40", + "A", + "A\x00", + "A1", + "ZZZZ", + "a", + "a\x00", + "aa", + "azz", + "}", + "\x7E", + "\x7F", + "\x80", + "\xFF", + "\u0100", + "\u01FF", + "\u0200", + "\u03FF", + "\u0400", + "\u07FF", + "\u0800", + "\u0FFF", + "\u1000", + "\u1FFF", + "\u2000", + "\u3FFF", + "\u4000", + "\u7FFF", + "\u8000", + "\uD800", + "\uD800a", + "\uD800\uDC01", + "\uDBFF", + "\uDC00", + "\uDFFF\uD800", + "\uFFFE", + "\uFFFF", + "\uFFFF\x00", + "\uFFFFZZZ", + [], + [-1/0], + [-1], + [0], + [1], + [1, "a"], + [1, []], + [1, [""]], + [2, 3], + [2, 3.0000000000001], + [12, [[]]], + [12, [[[]]]], + [12, [[[""]]]], + [12, [[["foo"]]]], + [12, [[[[[3]]]]]], + [12, [[[[[[3]]]]]]], + [12, [[[[[[3],[[[[[4.2]]]]]]]]]]], + [new Date(-1)], + [new Date(1)], + [""], + ["", [[]]], + ["", [[[]]]], + ["abc"], + ["abc", "def"], + ["abc\x00"], + ["abc\x00", "\x00\x01"], + ["abc\x00", "\x00def"], + ["abc\x00\x00def"], + ["x", [[]]], + ["x", [[[]]]], + [[]], + [[],"foo"], + [[],[]], + [[[]]], + [[[]], []], + [[[]], [[]]], + [[[]], [[1]]], + [[[]], [[[]]]], + [[[1]]], + [[[[]], []]], + ]; + const testString = + "abcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()-_+=,<.>/?\\|"; + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + info("Installing profile"); + + installPackagedProfile(testName + "_profile"); + + info("Opening database with no version"); + + let request = indexedDB.open(testName); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + is(db.version, 1, "Correct db version"); + + let transaction = db.transaction(testName); + transaction.oncomplete = grabEventAndContinueHandler; + + let objectStore = transaction.objectStore(testName); + let index = objectStore.index("uniqueIndex"); + + info("Starting 'uniqueIndex' cursor"); + + let keyIndex = 0; + index.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + info("Comparing " + JSON.stringify(cursor.primaryKey) + " to " + + JSON.stringify(testKeys[cursor.key]) + + " [" + cursor.key + "]"); + is(indexedDB.cmp(cursor.primaryKey, testKeys[cursor.key]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(cursor.primaryKey, testKeys[cursor.key]), true, + "Keys compare equally via 'compareKeys'"); + + let indexProperty = cursor.value.index; + is(Array.isArray(indexProperty), true, "index property is Array"); + is(indexProperty[0], cursor.key, "index property first item correct"); + is(indexProperty[1], cursor.key + 1, "index property second item correct"); + + is(cursor.key, keyIndex, "Cursor key property is correct"); + + is(cursor.value.testString, testString, "Test string compared equally"); + + keyIndex++; + cursor.continue(); + } + }; + yield undefined; + + is(keyIndex, testKeys.length, "Saw all keys"); + + transaction = db.transaction(testName, "readwrite"); + transaction.oncomplete = grabEventAndContinueHandler; + + objectStore = transaction.objectStore(testName); + index = objectStore.index("index"); + + info("Getting all 'index' keys"); + + index.getAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.length, testKeys.length * 2, "Got all keys"); + + info("Starting objectStore cursor"); + + objectStore.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + let value = cursor.value; + is(value.testString, testString, "Test string compared equally"); + + delete value.index; + cursor.update(value); + + cursor.continue(); + } else { + continueToNextStepSync(); + } + }; + yield undefined; + + info("Getting all 'index' keys"); + + index.getAllKeys().onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result.length, 0, "Removed all keys"); + yield undefined; + + db.close(); + + info("Opening database with new version"); + + request = indexedDB.open(testName, 2); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Deleting indexes"); + + objectStore = event.target.transaction.objectStore(testName); + objectStore.deleteIndex("index"); + objectStore.deleteIndex("uniqueIndex"); + + event = yield undefined; + + db = event.target.result; + + transaction = db.transaction(testName, "readwrite"); + transaction.oncomplete = grabEventAndContinueHandler; + + info("Starting objectStore cursor"); + + objectStore = transaction.objectStore(testName); + objectStore.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + let value = cursor.value; + is(value.testString, testString, "Test string compared equally"); + + value.index = value.keyPath; + cursor.update(value); + + cursor.continue(); + } + }; + event = yield undefined; + + db.close(); + + info("Opening database with new version"); + + request = indexedDB.open(testName, 3); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + info("Creating indexes"); + + objectStore = event.target.transaction.objectStore(testName); + objectStore.createIndex("index", "index"); + + event = yield undefined; + + db = event.target.result; + + transaction = db.transaction(testName); + transaction.oncomplete = grabEventAndContinueHandler; + + objectStore = transaction.objectStore(testName); + index = objectStore.index("index"); + + info("Starting 'index' cursor"); + + keyIndex = 0; + index.openCursor().onsuccess = event => { + let cursor = event.target.result; + if (cursor) { + is(indexedDB.cmp(cursor.primaryKey, testKeys[keyIndex]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(cursor.primaryKey, testKeys[keyIndex]), true, + "Keys compare equally via 'compareKeys'"); + is(indexedDB.cmp(cursor.key, testKeys[keyIndex]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(cursor.key, testKeys[keyIndex]), true, + "Keys compare equally via 'compareKeys'"); + + let indexProperty = cursor.value.index; + is(indexedDB.cmp(indexProperty, testKeys[keyIndex]), 0, + "Keys compare equally via 'indexedDB.cmp'"); + is(compareKeys(indexProperty, testKeys[keyIndex]), true, + "Keys compare equally via 'compareKeys'"); + + is(cursor.value.testString, testString, "Test string compared equally"); + + keyIndex++; + cursor.continue(); + } + }; + yield undefined; + + is(keyIndex, testKeys.length, "Added all keys again"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_schema23upgrade.js b/dom/indexedDB/test/unit/test_schema23upgrade.js new file mode 100644 index 000000000..47c9c2986 --- /dev/null +++ b/dom/indexedDB/test/unit/test_schema23upgrade.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const openParams = [ + // This one lives in storage/default/http+++www.mozilla.org + { url: "http://www.mozilla.org", dbName: "dbB", dbVersion: 1 }, + + // This one lives in storage/default/1007+t+https+++developer.cdn.mozilla.net + { appId: 1007, inIsolatedMozBrowser: true, url: "https://developer.cdn.mozilla.net", + dbName: "dbN", dbVersion: 1 }, + ]; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase(params) { + let uri = ios.newURI(params.url, null, null); + let principal = + ssm.createCodebasePrincipal(uri, + {appId: params.appId || ssm.NO_APPID, + inIsolatedMozBrowser: params.inIsolatedMozBrowser}); + let request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbVersion); + return request; + } + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("schema23upgrade_profile"); + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_setVersion.js b/dom/indexedDB/test/unit/test_setVersion.js new file mode 100644 index 000000000..cb2400b5d --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion.js @@ -0,0 +1,51 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.close(); + + // Check default state. + is(db.version, 1, "Correct default version for a new database."); + + const versions = [ + 7, + 42, + ]; + + for (let i = 0; i < versions.length; i++) { + let version = versions[i]; + + let request = indexedDB.open(name, version); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + is(db.version, version, "Database version number updated correctly"); + is(event.target.transaction.mode, "versionchange", "Correct mode"); + + // Wait for success + yield undefined; + + db.close(); + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_setVersion_abort.js b/dom/indexedDB/test/unit/test_setVersion_abort.js new file mode 100644 index 000000000..262c10346 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_abort.js @@ -0,0 +1,97 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = unexpectedSuccessHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + let objectStore = db.createObjectStore("foo"); + let index = objectStore.createIndex("bar", "baz"); + + is(db.version, 1, "Correct version"); + is(db.objectStoreNames.length, 1, "Correct objectStoreNames length"); + is(objectStore.indexNames.length, 1, "Correct indexNames length"); + + let transaction = event.target.transaction; + is(transaction.mode, "versionchange", "Correct transaction mode"); + transaction.oncomplete = unexpectedSuccessHandler; + transaction.onabort = grabEventAndContinueHandler; + transaction.abort(); + + is(db.version, 0, "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + is(objectStore.indexNames.length, 0, "Correct indexNames length"); + + // Test that the db is actually closed. + try { + db.transaction(""); + ok(false, "Expect an exception"); + } catch (e) { + ok(true, "Expect an exception"); + is(e.name, "InvalidStateError", "Expect an InvalidStateError"); + } + + event = yield undefined; + is(event.type, "abort", "Got transaction abort event"); + is(event.target, transaction, "Right target"); + + is(db.version, 0, "Correct version"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length"); + is(objectStore.indexNames.length, 0, "Correct indexNames length"); + + request.onerror = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + + event = yield undefined; + + is(event.type, "error", "Got request error event"); + is(event.target, request, "Right target"); + is(event.target.transaction, null, "No transaction"); + + event.preventDefault(); + + request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = unexpectedSuccessHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + let db2 = event.target.result; + + isnot(db, db2, "Should give a different db instance"); + is(db2.version, 1, "Correct version"); + is(db2.objectStoreNames.length, 0, "Correct objectStoreNames length"); + + let objectStore2 = db2.createObjectStore("foo"); + let index2 = objectStore2.createIndex("bar", "baz"); + + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + event = yield undefined; + + is(event.target.result, db2, "Correct target"); + is(event.type, "success", "Got success event"); + is(db2.version, 1, "Correct version"); + is(db2.objectStoreNames.length, 1, "Correct objectStoreNames length"); + is(objectStore2.indexNames.length, 1, "Correct indexNames length"); + is(db.version, 0, "Correct version still"); + is(db.objectStoreNames.length, 0, "Correct objectStoreNames length still"); + is(objectStore.indexNames.length, 0, "Correct indexNames length still"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_setVersion_events.js b/dom/indexedDB/test/unit/test_setVersion_events.js new file mode 100644 index 000000000..1bd757dc8 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_events.js @@ -0,0 +1,165 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + // Open a datbase for the first time. + let request = indexedDB.open(name, 1); + + // Sanity checks + ok(request instanceof IDBRequest, "Request should be an IDBRequest"); + ok(request instanceof IDBOpenDBRequest, "Request should be an IDBOpenDBRequest"); + ok(request instanceof EventTarget, "Request should be an EventTarget"); + is(request.source, null, "Request should have no source"); + try { + request.result; + ok(false, "Getter should have thrown!"); + } catch (e if e.result == 0x8053000b /* NS_ERROR_DOM_INVALID_STATE_ERR */) { + ok(true, "Getter threw the right exception"); + } + + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let versionChangeEventCount = 0; + let db1, db2, db3; + + db1 = event.target.result; + db1.addEventListener("versionchange", function(event) { + ok(true, "Got version change event"); + ok(event instanceof IDBVersionChangeEvent, "Event is of the right type"); + is("source" in event.target, false, "Correct source"); + is(event.target, db1, "Correct target"); + is(event.target.version, 1, "Correct db version"); + is(event.oldVersion, 1, "Correct event oldVersion"); + is(event.newVersion, 2, "Correct event newVersion"); + is(versionChangeEventCount++, 0, "Correct count"); + db1.close(); + }, false); + + // Open the database again and trigger an upgrade that should succeed + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onsuccess = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onblocked = errorHandler; + + event = yield undefined; + + // Test the upgradeneeded event. + ok(event instanceof IDBVersionChangeEvent, "Event is of the right type"); + ok(event.target.result instanceof IDBDatabase, "Good result"); + db2 = event.target.result; + is(event.target.transaction.mode, "versionchange", + "Correct mode"); + is(db2.version, 2, "Correct db version"); + is(event.oldVersion, 1, "Correct event oldVersion"); + is(event.newVersion, 2, "Correct event newVersion"); + + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + db2.addEventListener("versionchange", function(event) { + ok(true, "Got version change event"); + ok(event instanceof IDBVersionChangeEvent, "Event is of the right type"); + is("source" in event.target, false, "Correct source"); + is(event.target, db2, "Correct target"); + is(event.target.version, 2, "Correct db version"); + is(event.oldVersion, 2, "Correct event oldVersion"); + is(event.newVersion, 3, "Correct event newVersion"); + is(versionChangeEventCount++, 1, "Correct count"); + }, false); + + // Test opening the existing version again + request = indexedDB.open(name, 2); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + request.onblocked = errorHandler; + + event = yield undefined; + + db3 = event.target.result; + + // Test an upgrade that should fail + request = indexedDB.open(name, 3); + request.onerror = errorHandler; + request.onsuccess = errorHandler; + request.onupgradeneeded = errorHandler; + request.onblocked = grabEventAndContinueHandler; + + event = yield undefined; + ok(true, "Got version change blocked event"); + ok(event instanceof IDBVersionChangeEvent, "Event is of the right type"); + is(event.target.source, null, "Correct source"); + is(event.target.transaction, null, "Correct transaction"); + is(event.target, request, "Correct target"); + is(db3.version, 2, "Correct db version"); + is(event.oldVersion, 2, "Correct event oldVersion"); + is(event.newVersion, 3, "Correct event newVersion"); + versionChangeEventCount++; + db2.close(); + db3.close(); + + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + event = yield undefined; + + db3 = event.target.result; + db3.close(); + + // Test another upgrade that should succeed. + request = indexedDB.open(name, 4); + request.onerror = errorHandler; + request.onsuccess = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onblocked = errorHandler; + + event = yield undefined; + + ok(event instanceof IDBVersionChangeEvent, "Event is of the right type"); + ok(event.target.result instanceof IDBDatabase, "Good result"); + is(event.target.transaction.mode, "versionchange", + "Correct mode"); + is(event.oldVersion, 3, "Correct event oldVersion"); + is(event.newVersion, 4, "Correct event newVersion"); + + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + ok(event.target.result instanceof IDBDatabase, "Expect a database here"); + is(event.target.result.version, 4, "Right version"); + is(db3.version, 3, "After closing the version should not change!"); + is(db2.version, 2, "After closing the version should not change!"); + is(db1.version, 1, "After closing the version should not change!"); + + is(versionChangeEventCount, 3, "Saw all expected events"); + + event = new IDBVersionChangeEvent("versionchange"); + ok(event, "Should be able to create an event with just passing in the type"); + event = new IDBVersionChangeEvent("versionchange", {oldVersion: 1}); + ok(event, "Should be able to create an event with just the old version"); + is(event.oldVersion, 1, "Correct old version"); + is(event.newVersion, null, "Correct new version"); + event = new IDBVersionChangeEvent("versionchange", {newVersion: 1}); + ok(event, "Should be able to create an event with just the new version"); + is(event.oldVersion, 0, "Correct old version"); + is(event.newVersion, 1, "Correct new version"); + event = new IDBVersionChangeEvent("versionchange", {oldVersion: 1, newVersion: 2}); + ok(event, "Should be able to create an event with both versions"); + is(event.oldVersion, 1, "Correct old version"); + is(event.newVersion, 2, "Correct new version"); + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_setVersion_exclusion.js b/dom/indexedDB/test/unit/test_setVersion_exclusion.js new file mode 100644 index 000000000..2a51ab090 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_exclusion.js @@ -0,0 +1,95 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let request2 = indexedDB.open(name, 2); + request2.onerror = errorHandler; + request2.onupgradeneeded = unexpectedSuccessHandler; + request2.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + is(event.target, request, "Event should be fired on the request"); + ok(event.target.result instanceof IDBDatabase, "Expect a database here"); + + let db = event.target.result; + is(db.version, 1, "Database has correct version"); + + db.onupgradeneeded = function() { + ok(false, "our ongoing VERSION_CHANGE transaction should exclude any others!"); + } + + db.createObjectStore("foo"); + + try { + db.transaction("foo"); + ok(false, "Transactions should be disallowed now!"); + } catch (e) { + ok(e instanceof DOMException, "Expect a DOMException"); + is(e.name, "InvalidStateError", "Expect an InvalidStateError"); + is(e.code, DOMException.INVALID_STATE_ERR, "Expect an INVALID_STATE_ERR"); + } + + request.onupgradeneeded = unexpectedSuccessHandler; + request.transaction.oncomplete = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "complete", "Got complete event"); + + try { + db.transaction("foo"); + ok(true, "Transactions should be allowed now!"); + } catch (e) { + ok(false, "Transactions should be allowed now!"); + } + + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "success", "Expect a success event"); + is(event.target.result, db, "Same database"); + + db.onversionchange = function() { + ok(true, "next setVersion was unblocked appropriately"); + db.close(); + } + + try { + db.transaction("foo"); + ok(true, "Transactions should be allowed now!"); + } catch (e) { + ok(false, "Transactions should be allowed now!"); + } + + request.onsuccess = unexpectedSuccessHandler; + request2.onupgradeneeded = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "upgradeneeded", "Expect an upgradeneeded event"); + + db = event.target.result; + is(db.version, 2, "Database has correct version"); + + request2.onupgradeneeded = unexpectedSuccessHandler; + request2.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "success", "Expect a success event"); + is(event.target.result, db, "Same database"); + is(db.version, 2, "Database has correct version"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_setVersion_throw.js b/dom/indexedDB/test/unit/test_setVersion_throw.js new file mode 100644 index 000000000..a589bb385 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_throw.js @@ -0,0 +1,54 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "test_setVersion_throw"; + + // This test requires two databases. The first needs to be a low version + // number that gets closed when a second higher version number database is + // created. Then the upgradeneeded event for the second database throws an + // exception and triggers an abort/close. + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + request.onupgradeneeded = function(event) { + info("Got upgradeneeded event for db 1"); + }; + let event = yield undefined; + + is(event.type, "success", "Got success event for db 1"); + + let db = event.target.result; + db.onversionchange = function(event) { + info("Got versionchange event for db 1"); + event.target.close(); + } + + executeSoon(continueToNextStepSync); + yield undefined; + + request = indexedDB.open(name, 2); + request.onerror = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + request.onupgradeneeded = function(event) { + info("Got upgradeneeded event for db 2"); + expectUncaughtException(true); + trigger_js_exception_by_calling_a_nonexistent_function(); + }; + event = yield undefined; + + event.preventDefault(); + + is(event.type, "error", "Got an error event for db 2"); + ok(event.target.error instanceof DOMError, "Request has a DOMError"); + is(event.target.error.name, "AbortError", "Request has AbortError"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_snappyUpgrade.js b/dom/indexedDB/test/unit/test_snappyUpgrade.js new file mode 100644 index 000000000..c2c1b1668 --- /dev/null +++ b/dom/indexedDB/test/unit/test_snappyUpgrade.js @@ -0,0 +1,44 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = "test_snappyUpgrade.js"; + const objectStoreName = "test"; + const testString = "Lorem ipsum his ponderum delicatissimi ne, at noster dolores urbanitas pro, cibo elaboraret no his. Ea dicunt maiorum usu. Ad appareat facilisis mediocritatem eos. Tale graeci mentitum in eos, hinc insolens at nam. Graecis nominavi aliquyam eu vix. Id solet assentior sadipscing pro. Et per atqui graecis, usu quot viris repudiandae ei, mollis evertitur an nam. At nam dolor ignota, liber labore omnesque ea mei, has movet voluptaria in. Vel an impetus omittantur. Vim movet option salutandi ex, ne mei ignota corrumpit. Mucius comprehensam id per. Est ea putant maiestatis."; + + info("Installing profile"); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("snappyUpgrade_profile"); + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Getting string"); + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(1); + request.onsuccess = continueToNextStepSync; + yield undefined; + + is(request.result, testString); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_storagePersistentUpgrade.js b/dom/indexedDB/test/unit/test_storagePersistentUpgrade.js new file mode 100644 index 000000000..ce138f138 --- /dev/null +++ b/dom/indexedDB/test/unit/test_storagePersistentUpgrade.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const openParams = [ + // This one lives in storage/default/http+++www.mozilla.org + { url: "http://www.mozilla.org", dbName: "dbB", dbVersion: 1 }, + + // This one lives in storage/default/1007+t+https+++developer.cdn.mozilla.net + { appId: 1007, inIsolatedMozBrowser: true, url: "https://developer.cdn.mozilla.net", + dbName: "dbN", dbVersion: 1 }, + ]; + + let ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + + let ssm = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager); + + function openDatabase(params) { + let uri = ios.newURI(params.url, null, null); + let principal = + ssm.createCodebasePrincipal(uri, + {appId: params.appId || ssm.NO_APPID, + inIsolatedMozBrowser: params.inIsolatedMozBrowser}); + let request = indexedDB.openForPrincipal(principal, params.dbName, + params.dbVersion); + return request; + } + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("storagePersistentUpgrade_profile"); + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + for (let params of openParams) { + let request = openDatabase(params); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Correct event type"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_storage_manager_estimate.js b/dom/indexedDB/test/unit/test_storage_manager_estimate.js new file mode 100644 index 000000000..0e3dc2d35 --- /dev/null +++ b/dom/indexedDB/test/unit/test_storage_manager_estimate.js @@ -0,0 +1,63 @@ +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : + "test_storage_manager_estimate.js"; + const objectStoreName = "storagesManager"; + const arraySize = 1e6; + + ok('estimate' in navigator.storage, 'Has estimate function'); + is(typeof navigator.storage.estimate, 'function', 'estimate is function'); + ok(navigator.storage.estimate() instanceof Promise, + 'estimate() method exists and returns a Promise'); + + navigator.storage.estimate().then(estimation => { + testGenerator.send(estimation.usage); + }); + + let before = yield undefined; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = continueToNextStep; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + let objectStore = db.createObjectStore(objectStoreName, { }); + yield undefined; + + navigator.storage.estimate().then(estimation => { + testGenerator.send(estimation.usage); + }); + let usageAfterCreate = yield undefined; + ok(usageAfterCreate > before, 'estimated usage must increase after createObjectStore'); + + let txn = db.transaction(objectStoreName, "readwrite"); + objectStore = txn.objectStore(objectStoreName); + objectStore.put(new Uint8Array(arraySize), 'k'); + txn.oncomplete = continueToNextStep; + txn.onabort = errorHandler; + txn.onerror = errorHandler; + event = yield undefined; + + navigator.storage.estimate().then(estimation => { + testGenerator.send(estimation.usage); + }); + let usageAfterPut = yield undefined; + ok(usageAfterPut > usageAfterCreate, 'estimated usage must increase after putting large object'); + db.close(); + + finishTest(); + yield undefined; +} + +function setup() +{ + SpecialPowers.pushPrefEnv({ + "set": [["dom.storageManager.enabled", true]] + }, runTest); +} diff --git a/dom/indexedDB/test/unit/test_success_events_after_abort.js b/dom/indexedDB/test/unit/test_success_events_after_abort.js new file mode 100644 index 000000000..efd138370 --- /dev/null +++ b/dom/indexedDB/test/unit/test_success_events_after_abort.js @@ -0,0 +1,60 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + + event.target.onsuccess = continueToNextStep; + + let objectStore = db.createObjectStore("foo"); + objectStore.add({}, 1).onerror = errorHandler; + + yield undefined; + + objectStore = db.transaction("foo").objectStore("foo"); + + let transaction = objectStore.transaction; + transaction.oncomplete = unexpectedSuccessHandler; + transaction.onabort = grabEventAndContinueHandler; + + let sawError = false; + + request = objectStore.get(1); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = function(event) { + is(event.target.error.name, "AbortError", "Good error"); + sawError = true; + event.preventDefault(); + } + + transaction.abort(); + + event = yield undefined; + + is(event.type, "abort", "Got abort event"); + is(sawError, true, "Saw get() error"); + if (this.window) { + // Make sure the success event isn't queued somehow. + let comp = SpecialPowers.wrap(Components); + let thread = comp.classes["@mozilla.org/thread-manager;1"] + .getService(comp.interfaces.nsIThreadManager) + .currentThread; + while (thread.hasPendingEvents()) { + thread.processNextEvent(false); + } + } + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_table_locks.js b/dom/indexedDB/test/unit/test_table_locks.js new file mode 100644 index 000000000..900f78edf --- /dev/null +++ b/dom/indexedDB/test/unit/test_table_locks.js @@ -0,0 +1,116 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const dbName = ("window" in this) ? window.location.pathname : "test"; +const dbVersion = 1; +const objName1 = "o1"; +const objName2 = "o2"; +const idxName1 = "i1"; +const idxName2 = "i2"; +const idxKeyPathProp = "idx"; +const objDataProp = "data"; +const objData = "1234567890"; +const objDataCount = 5; +const loopCount = 100; + +var testGenerator = testSteps(); + +function testSteps() +{ + let req = indexedDB.open(dbName, dbVersion); + req.onerror = errorHandler; + req.onupgradeneeded = grabEventAndContinueHandler; + req.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + let db = event.target.result; + + let objectStore1 = db.createObjectStore(objName1); + objectStore1.createIndex(idxName1, idxKeyPathProp); + + let objectStore2 = db.createObjectStore(objName2); + objectStore2.createIndex(idxName2, idxKeyPathProp); + + for (let i = 0; i < objDataCount; i++) { + var data = { }; + data[objDataProp] = objData; + data[idxKeyPathProp] = objDataCount - i - 1; + + objectStore1.add(data, i); + objectStore2.add(data, i); + } + + event = yield undefined; + + is(event.type, "success", "Got success event"); + + doReadOnlyTransaction(db, 0, loopCount); + doReadWriteTransaction(db, 0, loopCount); + + // Wait for readonly and readwrite transaction loops to complete. + yield undefined; + yield undefined; + + finishTest(); + yield undefined; +} + +function doReadOnlyTransaction(db, key, remaining) +{ + if (!remaining) { + info("Finished all readonly transactions"); + continueToNextStep(); + return; + } + + info("Starting readonly transaction for key " + key + ", " + remaining + + " loops left"); + + let objectStore = db.transaction(objName1, "readonly").objectStore(objName1); + let index = objectStore.index(idxName1); + + index.openKeyCursor(key, "prev").onsuccess = function(event) { + let cursor = event.target.result; + ok(cursor, "Got readonly cursor"); + + objectStore.get(cursor.primaryKey).onsuccess = function(event) { + if (++key == objDataCount) { + key = 0; + } + doReadOnlyTransaction(db, key, remaining - 1); + } + }; +} + +function doReadWriteTransaction(db, key, remaining) +{ + if (!remaining) { + info("Finished all readwrite transactions"); + continueToNextStep(); + return; + } + + info("Starting readwrite transaction for key " + key + ", " + remaining + + " loops left"); + + let objectStore = db.transaction(objName2, "readwrite").objectStore(objName2); + objectStore.openCursor(key).onsuccess = function(event) { + let cursor = event.target.result; + ok(cursor, "Got readwrite cursor"); + + let value = cursor.value; + value[idxKeyPathProp]++; + + cursor.update(value).onsuccess = function(event) { + if (++key == objDataCount) { + key = 0; + } + doReadWriteTransaction(db, key, remaining - 1); + } + }; +} diff --git a/dom/indexedDB/test/unit/test_table_rollback.js b/dom/indexedDB/test/unit/test_table_rollback.js new file mode 100644 index 000000000..afe194ad5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_table_rollback.js @@ -0,0 +1,115 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbName = ("window" in this) ? window.location.pathname : "test"; + const objName1 = "foo"; + const objName2 = "bar"; + const data1 = "1234567890"; + const data2 = "0987654321"; + const dataCount = 500; + + let request = indexedDB.open(dbName, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded"); + + request.onupgradeneeded = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + let db = request.result; + + let objectStore1 = db.createObjectStore(objName1, { autoIncrement: true }); + let objectStore2 = db.createObjectStore(objName2, { autoIncrement: true }); + + info("Created object stores, adding data"); + + for (let i = 0; i < dataCount; i++) { + objectStore1.add(data1); + objectStore2.add(data2); + } + + info("Done adding data"); + + event = yield undefined; + + is(event.type, "success", "Got success"); + + let readResult = null; + let readError = null; + let writeAborted = false; + + info("Creating readwrite transaction"); + + objectStore1 = db.transaction(objName1, "readwrite").objectStore(objName1); + objectStore1.openCursor().onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + let cursor = event.target.result; + is(cursor.value, data1, "Got correct data for readwrite transaction"); + + info("Modifying object store on readwrite transaction"); + + cursor.update(data2); + cursor.continue(); + + event = yield undefined; + + info("Done modifying object store on readwrite transaction, creating " + + "readonly transaction"); + + objectStore2 = db.transaction(objName2, "readonly").objectStore(objName2); + request = objectStore2.getAll(); + request.onsuccess = function(event) { + readResult = event.target.result; + is(readResult.length, + dataCount, + "Got correct number of results on readonly transaction"); + for (let i = 0; i < readResult.length; i++) { + is(readResult[i], data2, "Got correct data for readonly transaction"); + } + if (writeAborted) { + continueToNextStep(); + } + }; + request.onerror = function(event) { + readResult = null; + readError = event.target.error; + + ok(false, "Got read error: " + readError.name); + event.preventDefault(); + + if (writeAborted) { + continueToNextStep(); + } + } + + cursor = event.target.result; + is(cursor.value, data1, "Got correct data for readwrite transaction"); + + info("Aborting readwrite transaction"); + + cursor.source.transaction.abort(); + writeAborted = true; + + if (!readError && !readResult) { + info("Waiting for readonly transaction to complete"); + yield undefined; + } + + ok(readResult, "Got result from readonly transaction"); + is(readError, null, "No read error"); + is(writeAborted, true, "Aborted readwrite transaction"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_temporary_storage.js b/dom/indexedDB/test/unit/test_temporary_storage.js new file mode 100644 index 000000000..15426abbb --- /dev/null +++ b/dom/indexedDB/test/unit/test_temporary_storage.js @@ -0,0 +1,258 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? + window.location.pathname : + "test_temporary_storage.js"; + const finalVersion = 2; + + const tempStorageLimitKB = 1024; + const checkpointSleepTimeSec = 5; + + function getSpec(index) { + return "http://foo" + index + ".com"; + } + + for (let temporary of [true, false]) { + info("Testing '" + (temporary ? "temporary" : "default") + "' storage"); + + setTemporaryStorageLimit(tempStorageLimitKB); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + info("Stage 1 - Creating empty databases until we reach the quota limit"); + + let databases = []; + let options = { version: finalVersion }; + if (temporary) { + options.storage = "temporary"; + } + + while (true) { + let spec = getSpec(databases.length); + + info("Opening database for " + spec + " with version " + options.version); + + let gotUpgradeIncomplete = false; + let gotUpgradeComplete = false; + + let request = + indexedDB.openForPrincipal(getPrincipal(spec), name, options); + request.onerror = function(event) { + is(request.error.name, + gotUpgradeIncomplete ? "AbortError" : "QuotaExceededError", + "Reached quota limit"); + event.preventDefault(); + testGenerator.send(false); + } + request.onupgradeneeded = function(event) { + event.target.transaction.onabort = function(e) { + gotUpgradeIncomplete = true; + is(e.target.error.name, "QuotaExceededError", "Reached quota limit"); + } + event.target.transaction.oncomplete = function() { + gotUpgradeComplete = true; + } + } + request.onsuccess = function(event) { + let db = event.target.result; + is(db.version, finalVersion, "Correct version " + finalVersion); + databases.push(db); + testGenerator.send(true); + } + + let shouldContinue = yield undefined; + if (shouldContinue) { + is(gotUpgradeComplete, true, "Got upgradeneeded event"); + ok(true, "Got success event"); + } else { + break; + } + } + + while (true) { + info("Sleeping for " + checkpointSleepTimeSec + " seconds to let all " + + "checkpoints finish so that we know we have reached quota limit"); + setTimeout(continueToNextStepSync, checkpointSleepTimeSec * 1000); + yield undefined; + + let spec = getSpec(databases.length); + + info("Opening database for " + spec + " with version " + options.version); + + let gotUpgradeIncomplete = false; + let gotUpgradeComplete = false; + + let request = + indexedDB.openForPrincipal(getPrincipal(spec), name, options); + request.onerror = function(event) { + is(request.error.name, + gotUpgradeIncomplete ? "AbortError" : "QuotaExceededError", + "Reached quota limit"); + event.preventDefault(); + testGenerator.send(false); + } + request.onupgradeneeded = function(event) { + event.target.transaction.onabort = function(e) { + gotUpgradeIncomplete = true; + is(e.target.error.name, "QuotaExceededError", "Reached quota limit"); + } + event.target.transaction.oncomplete = function() { + gotUpgradeComplete = true; + } + } + request.onsuccess = function(event) { + let db = event.target.result; + is(db.version, finalVersion, "Correct version " + finalVersion); + databases.push(db); + testGenerator.send(true); + } + + let shouldContinue = yield undefined; + if (shouldContinue) { + is(gotUpgradeComplete, true, "Got upgradeneeded event"); + ok(true, "Got success event"); + } else { + break; + } + } + + let databaseCount = databases.length; + info("Created " + databaseCount + " databases before quota limit reached"); + + info("Stage 2 - " + + "Closing all databases and then attempting to create one more, then " + + "verifying that the oldest origin was cleared"); + + for (let i = 0; i < databases.length; i++) { + info("Closing database for " + getSpec(i)); + databases[i].close(); + + // Timer resolution on Windows is low so wait for 40ms just to be safe. + setTimeout(continueToNextStepSync, 40); + yield undefined; + } + databases = null; + + let spec = getSpec(databaseCount); + info("Opening database for " + spec + " with version " + options.version); + + let request = indexedDB.openForPrincipal(getPrincipal(spec), name, options); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got success event"); + + let db = event.target.result; + is(db.version, finalVersion, "Correct version " + finalVersion); + db.close(); + db = null; + + setTemporaryStorageLimit(tempStorageLimitKB * 2); + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + delete options.version; + + spec = getSpec(0); + info("Opening database for " + spec + " with unspecified version"); + + request = indexedDB.openForPrincipal(getPrincipal(spec), name, options); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + event = yield undefined; + + is(event.type, "upgradeneeded", "Got upgradeneeded event"); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got success event"); + + db = event.target.result; + is(db.version, 1, "Correct version 1 (database was recreated)"); + db.close(); + db = null; + + info("Stage 3 - " + + "Cutting storage limit in half to force deletion of some databases"); + + setTemporaryStorageLimit(tempStorageLimitKB / 2); + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + info("Opening database for " + spec + " with unspecified version"); + + // Open the same db again to force QM to delete others. The first origin (0) + // should be the most recent so it should not be deleted and we should not + // get an upgradeneeded event here. + request = indexedDB.openForPrincipal(getPrincipal(spec), name, options); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + db = event.target.result; + is(db.version, 1, "Correct version 1"); + db.close(); + db = null; + + setTemporaryStorageLimit(tempStorageLimitKB * 2); + + resetAllDatabases(continueToNextStepSync); + yield undefined; + + options.version = finalVersion; + + let newDatabaseCount = 0; + for (let i = 0; i < databaseCount; i++) { + let spec = getSpec(i); + info("Opening database for " + spec + " with version " + options.version); + + let request = + indexedDB.openForPrincipal(getPrincipal(spec), name, options); + request.onerror = errorHandler; + request.onupgradeneeded = function(event) { + if (!event.oldVersion) { + newDatabaseCount++; + } + } + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + is(event.type, "success", "Got correct event type"); + + let db = request.result; + is(db.version, finalVersion, "Correct version " + finalVersion); + db.close(); + } + + info("Needed to recreate " + newDatabaseCount + " databases"); + ok(newDatabaseCount, "Created some new databases"); + ok(newDatabaseCount < databaseCount, "Didn't recreate all databases"); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_traffic_jam.js b/dom/indexedDB/test/unit/test_traffic_jam.js new file mode 100644 index 000000000..f09985b13 --- /dev/null +++ b/dom/indexedDB/test/unit/test_traffic_jam.js @@ -0,0 +1,87 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let requests = []; + function doOpen(version, errorCallback, upgradeNeededCallback, successCallback) { + let request = indexedDB.open(name, version); + request.onerror = errorCallback; + request.onupgradeneeded = upgradeNeededCallback; + request.onsuccess = successCallback; + requests.push(request); + } + + doOpen(1, errorHandler, grabEventAndContinueHandler, grabEventAndContinueHandler); + doOpen(2, errorHandler, unexpectedSuccessHandler, unexpectedSuccessHandler); + + let event = yield undefined; + is(event.type, "upgradeneeded", "expect an upgradeneeded event"); + is(event.target, requests[0], "fired at the right request"); + + let db = event.target.result; + db.createObjectStore("foo"); + + doOpen(3, errorHandler, unexpectedSuccessHandler, unexpectedSuccessHandler); + doOpen(2, errorHandler, unexpectedSuccessHandler, unexpectedSuccessHandler); + doOpen(3, errorHandler, unexpectedSuccessHandler, unexpectedSuccessHandler); + + event.target.transaction.oncomplete = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "complete", "expect a complete event"); + is(event.target, requests[0].transaction, "expect it to be fired at the transaction"); + + event = yield undefined; + is(event.type, "success", "expect a success event"); + is(event.target, requests[0], "fired at the right request"); + event.target.result.close(); + + requests[1].onupgradeneeded = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "upgradeneeded", "expect an upgradeneeded event"); + is(event.target, requests[1], "fired at the right request"); + + requests[1].onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "success", "expect a success event"); + is(event.target, requests[1], "fired at the right request"); + event.target.result.close(); + + requests[2].onupgradeneeded = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "upgradeneeded", "expect an upgradeneeded event"); + is(event.target, requests[2], "fired at the right request"); + + requests[2].onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "success", "expect a success event"); + is(event.target, requests[2], "fired at the right request"); + event.target.result.close(); + + requests[3].onerror = null; + requests[3].addEventListener("error", new ExpectError("VersionError", true)); + + event = yield undefined; + + requests[4].onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "success", "expect a success event"); + is(event.target, requests[4], "fired at the right request"); + event.target.result.close(); + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_transaction_abort.js b/dom/indexedDB/test/unit/test_transaction_abort.js new file mode 100644 index 000000000..0f051f968 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_abort.js @@ -0,0 +1,384 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +var abortFired = false; + +function abortListener(evt) +{ + abortFired = true; + is(evt.target.error, null, "Expect a null error for an aborted transaction"); +} + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onabort = abortListener; + + let transaction; + let objectStore; + let index; + + transaction = event.target.transaction; + + is(transaction.error, null, "Expect a null error"); + + objectStore = db.createObjectStore("foo", { autoIncrement: true }); + index = objectStore.createIndex("fooindex", "indexKey", { unique: true }); + + is(transaction.db, db, "Correct database"); + is(transaction.mode, "versionchange", "Correct mode"); + is(transaction.objectStoreNames.length, 1, "Correct names length"); + is(transaction.objectStoreNames.item(0), "foo", "Correct name"); + is(transaction.objectStore("foo"), objectStore, "Can get stores"); + is(transaction.oncomplete, null, "No complete listener"); + is(transaction.onabort, null, "No abort listener"); + + is(objectStore.name, "foo", "Correct name"); + is(objectStore.keyPath, null, "Correct keyPath"); + + is(objectStore.indexNames.length, 1, "Correct indexNames length"); + is(objectStore.indexNames[0], "fooindex", "Correct indexNames name"); + is(objectStore.index("fooindex"), index, "Can get index"); + + // Wait until it's complete! + transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(transaction.db, db, "Correct database"); + is(transaction.mode, "versionchange", "Correct mode"); + is(transaction.objectStoreNames.length, 1, "Correct names length"); + is(transaction.objectStoreNames.item(0), "foo", "Correct name"); + is(transaction.onabort, null, "No abort listener"); + + try { + is(transaction.objectStore("foo").name, "foo", "Can't get stores"); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Out of scope transaction can't make stores"); + } + + is(objectStore.name, "foo", "Correct name"); + is(objectStore.keyPath, null, "Correct keyPath"); + + is(objectStore.indexNames.length, 1, "Correct indexNames length"); + is(objectStore.indexNames[0], "fooindex", "Correct indexNames name"); + + try { + objectStore.add({}); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Add threw"); + } + + try { + objectStore.put({}, 1); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Put threw"); + } + + try { + objectStore.put({}, 1); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Put threw"); + } + + try { + objectStore.delete(1); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Remove threw"); + } + + try { + objectStore.get(1); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Get threw"); + } + + try { + objectStore.getAll(null); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "GetAll threw"); + } + + try { + objectStore.openCursor(); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "OpenCursor threw"); + } + + try { + objectStore.createIndex("bar", "id"); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "CreateIndex threw"); + } + + try { + objectStore.index("bar"); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "Index threw"); + } + + try { + objectStore.deleteIndex("bar"); + ok(false, "Should have thrown"); + } + catch (e) { + ok(true, "RemoveIndex threw"); + } + + yield undefined; + + request = db.transaction("foo", "readwrite").objectStore("foo").add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + event.target.transaction.onabort = function(event) { + ok(false, "Shouldn't see an abort event!"); + }; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "complete", "Right kind of event"); + + let key; + + request = db.transaction("foo", "readwrite").objectStore("foo").add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + key = event.target.result; + + event.target.transaction.onabort = grabEventAndContinueHandler; + event.target.transaction.oncomplete = function(event) { + ok(false, "Shouldn't see a complete event here!"); + }; + + event.target.transaction.abort(); + + event = yield undefined; + + is(event.type, "abort", "Right kind of event"); + + request = db.transaction("foo").objectStore("foo").get(key); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Object was removed"); + + executeSoon(function() { testGenerator.next(); }); + yield undefined; + + let keys = []; + let abortEventCount = 0; + function abortErrorHandler(event) { + is(event.target.error.name, "AbortError", + "Good error"); + abortEventCount++; + event.preventDefault(); + }; + objectStore = db.transaction("foo", "readwrite").objectStore("foo"); + + for (let i = 0; i < 10; i++) { + request = objectStore.add({}); + request.onerror = abortErrorHandler; + request.onsuccess = function(event) { + keys.push(event.target.result); + if (keys.length == 5) { + event.target.transaction.onabort = grabEventAndContinueHandler; + event.target.transaction.abort(); + } + }; + } + event = yield undefined; + + is(event.type, "abort", "Got abort event"); + is(keys.length, 5, "Added 5 items in this transaction"); + is(abortEventCount, 5, "Got 5 abort error events"); + + for (let i in keys) { + request = db.transaction("foo").objectStore("foo").get(keys[i]); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(event.target.result, undefined, "Object was removed by abort"); + } + + // Set up some predictible data + transaction = db.transaction("foo", "readwrite"); + objectStore = transaction.objectStore("foo"); + objectStore.clear(); + objectStore.add({}, 1); + objectStore.add({}, 2); + request = objectStore.add({}, 1); + request.onsuccess = function() { + ok(false, "inserting duplicate key should fail"); + } + request.onerror = function(event) { + ok(true, "inserting duplicate key should fail"); + event.preventDefault(); + } + transaction.oncomplete = grabEventAndContinueHandler; + yield undefined; + + // Check when aborting is allowed + abortEventCount = 0; + let expectedAbortEventCount = 0; + + // During INITIAL + transaction = db.transaction("foo"); + transaction.abort(); + try { + transaction.abort(); + ok(false, "second abort should throw an error"); + } + catch (ex) { + ok(true, "second abort should throw an error"); + } + + // During LOADING + transaction = db.transaction("foo"); + transaction.objectStore("foo").get(1).onerror = abortErrorHandler; + expectedAbortEventCount++; + transaction.abort(); + try { + transaction.abort(); + ok(false, "second abort should throw an error"); + } + catch (ex) { + ok(true, "second abort should throw an error"); + } + + // During LOADING from callback + transaction = db.transaction("foo"); + transaction.objectStore("foo").get(1).onsuccess = grabEventAndContinueHandler; + event = yield undefined; + transaction.objectStore("foo").get(1).onerror = abortErrorHandler; + expectedAbortEventCount++ + transaction.abort(); + try { + transaction.abort(); + ok(false, "second abort should throw an error"); + } + catch (ex) { + ok(true, "second abort should throw an error"); + } + + // During LOADING from error callback + transaction = db.transaction("foo", "readwrite"); + transaction.objectStore("foo").add({}, 1).onerror = function(event) { + event.preventDefault(); + + transaction.objectStore("foo").get(1).onerror = abortErrorHandler; + expectedAbortEventCount++ + + transaction.abort(); + continueToNextStep(); + } + yield undefined; + + // In between callbacks + transaction = db.transaction("foo"); + function makeNewRequest() { + let r = transaction.objectStore("foo").get(1); + r.onsuccess = makeNewRequest; + r.onerror = abortErrorHandler; + } + makeNewRequest(); + transaction.objectStore("foo").get(1).onsuccess = function(event) { + executeSoon(function() { + transaction.abort(); + expectedAbortEventCount++; + continueToNextStep(); + }); + }; + yield undefined; + + // During COMMITTING + transaction = db.transaction("foo", "readwrite"); + transaction.objectStore("foo").put({hello: "world"}, 1).onsuccess = function(event) { + continueToNextStep(); + }; + yield undefined; + try { + transaction.abort(); + ok(false, "second abort should throw an error"); + } + catch (ex) { + ok(true, "second abort should throw an error"); + } + transaction.oncomplete = grabEventAndContinueHandler; + event = yield undefined; + + // Since the previous transaction shouldn't have caused any error events, + // we know that all events should have fired by now. + is(abortEventCount, expectedAbortEventCount, + "All abort errors fired"); + + // Abort both failing and succeeding requests + transaction = db.transaction("foo", "readwrite"); + transaction.onabort = transaction.oncomplete = grabEventAndContinueHandler; + transaction.objectStore("foo").add({indexKey: "key"}).onsuccess = function(event) { + transaction.abort(); + }; + let request1 = transaction.objectStore("foo").add({indexKey: "key"}); + request1.onsuccess = grabEventAndContinueHandler; + request1.onerror = grabEventAndContinueHandler; + let request2 = transaction.objectStore("foo").get(1); + request2.onsuccess = grabEventAndContinueHandler; + request2.onerror = grabEventAndContinueHandler; + + event = yield undefined; + is(event.type, "error", "abort() should make all requests fail"); + is(event.target, request1, "abort() should make all requests fail"); + is(event.target.error.name, "AbortError", "abort() should make all requests fail"); + event.preventDefault(); + + event = yield undefined; + is(event.type, "error", "abort() should make all requests fail"); + is(event.target, request2, "abort() should make all requests fail"); + is(event.target.error.name, "AbortError", "abort() should make all requests fail"); + event.preventDefault(); + + event = yield undefined; + is(event.type, "abort", "transaction should fail"); + is(event.target, transaction, "transaction should fail"); + + ok(abortFired, "Abort should have fired!"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_transaction_abort_hang.js b/dom/indexedDB/test/unit/test_transaction_abort_hang.js new file mode 100644 index 000000000..cb59174f6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_abort_hang.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var self = this; + +var testGenerator = testSteps(); + +function testSteps() +{ + const dbName = self.window ? + window.location.pathname : + "test_transaction_abort_hang"; + const objStoreName = "foo"; + const transactionCount = 30; + + let completedTransactionCount = 0; + let caughtError = false; + + let abortedTransactionIndex = Math.floor(transactionCount / 2); + if (abortedTransactionIndex % 2 == 0) { + abortedTransactionIndex++; + } + + let request = indexedDB.open(dbName, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + request.result.createObjectStore(objStoreName, { autoIncrement: true }); + + request.onupgradeneeded = null; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let db = event.target.result; + + for (let i = 0; i < transactionCount; i++) { + const readonly = i % 2 == 0; + const mode = readonly ? "readonly" : "readwrite"; + + let transaction = db.transaction(objStoreName, mode); + + if (i == transactionCount - 1) { + // Last one, finish the test. + transaction.oncomplete = grabEventAndContinueHandler; + } else if (i == abortedTransactionIndex - 1) { + transaction.oncomplete = function(event) { + ok(true, "Completed transaction " + ++completedTransactionCount + + " (We may hang after this!)"); + }; + } else if (i == abortedTransactionIndex) { + // Special transaction that we abort outside the normal event flow. + transaction.onerror = function(event) { + ok(true, "Aborted transaction " + ++completedTransactionCount + + " (We didn't hang!)"); + is(event.target.error.name, "AbortError", + "AbortError set as the error on the request"); + is(event.target.transaction.error, null, + "No error set on the transaction"); + ok(!caughtError, "Haven't seen the error event yet"); + caughtError = true; + event.preventDefault(); + }; + // This has to happen after the we return to the event loop but before the + // transaction starts running. + executeSoon(function() { transaction.abort(); }); + } else { + transaction.oncomplete = function(event) { + ok(true, "Completed transaction " + ++completedTransactionCount); + }; + } + + if (readonly) { + transaction.objectStore(objStoreName).get(0); + } else { + try { transaction.objectStore(objStoreName).add({}); } catch(e) { } + } + } + ok(true, "Created all transactions"); + + event = yield undefined; + + ok(true, "Completed transaction " + ++completedTransactionCount); + ok(caughtError, "Caught the error event when we aborted the transaction"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_transaction_duplicate_store_names.js b/dom/indexedDB/test/unit/test_transaction_duplicate_store_names.js new file mode 100644 index 000000000..f37b14bab --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_duplicate_store_names.js @@ -0,0 +1,43 @@ + +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() { + const dbName = this.window ? + window.location.pathname : + "test_transaction_duplicate_store_names"; + const dbVersion = 1; + const objectStoreName = "foo"; + const data = { }; + const dataKey = 1; + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName); + objectStore.add(data, dataKey); + + event = yield undefined; + + db = event.target.result; + + let transaction = db.transaction([objectStoreName, objectStoreName], "readwrite"); + transaction.onerror = errorHandler; + transaction.oncomplete = grabEventAndContinueHandler; + + event = yield undefined; + + ok(true, "Transaction created successfully"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_transaction_error.js b/dom/indexedDB/test/unit/test_transaction_error.js new file mode 100644 index 000000000..14217eba8 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_error.js @@ -0,0 +1,136 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() { + const dbName = this.window ? + window.location.pathname : + "test_transaction_error"; + const dbVersion = 1; + const objectStoreName = "foo"; + const data = { }; + const dataKey = 1; + const expectedError = "ConstraintError"; + + let request = indexedDB.open(dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + + let event = yield undefined; + + info("Creating database"); + + let db = event.target.result; + let objectStore = db.createObjectStore(objectStoreName); + objectStore.add(data, dataKey); + + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + db = event.target.result; + + try { + db.transaction(objectStoreName, "versionchange"); + ok(false, "TypeError shall be thrown if transaction mode is wrong."); + } catch (e) { + ok(e instanceof DOMException, "got a database exception"); + is(e.name, "TypeError", "correct error"); + } + + let transaction = db.transaction(objectStoreName, "readwrite"); + transaction.onerror = grabEventAndContinueHandler; + transaction.oncomplete = grabEventAndContinueHandler; + + objectStore = transaction.objectStore(objectStoreName); + + info("Adding duplicate entry with preventDefault()"); + + request = objectStore.add(data, dataKey); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "error", "Got an error event"); + is(event.target, request, "Error event targeted request"); + is(event.currentTarget, request, "Got request error first"); + is(event.currentTarget.error.name, expectedError, + "Request has correct error"); + event.preventDefault(); + + event = yield undefined; + + is(event.type, "error", "Got an error event"); + is(event.target, request, "Error event targeted request"); + is(event.currentTarget, transaction, "Got transaction error second"); + is(event.currentTarget.error, null, "Transaction has null error"); + + event = yield undefined; + + is(event.type, "complete", "Got a complete event"); + is(event.target, transaction, "Complete event targeted transaction"); + is(event.currentTarget, transaction, + "Complete event only targeted transaction"); + is(event.currentTarget.error, null, "Transaction has null error"); + + // Try again without preventDefault(). + + transaction = db.transaction(objectStoreName, "readwrite"); + transaction.onerror = grabEventAndContinueHandler; + transaction.onabort = grabEventAndContinueHandler; + + objectStore = transaction.objectStore(objectStoreName); + + info("Adding duplicate entry without preventDefault()"); + + if ("SimpleTest" in this) { + SimpleTest.expectUncaughtException(); + } else if ("DedicatedWorkerGlobalScope" in self && + self instanceof DedicatedWorkerGlobalScope) { + let oldErrorFunction = self.onerror; + self.onerror = function(message, file, line) { + self.onerror = oldErrorFunction; + oldErrorFunction = null; + + is(message, + "ConstraintError", + "Got expected ConstraintError on DedicatedWorkerGlobalScope"); + return true; + }; + } + + request = objectStore.add(data, dataKey); + request.onsuccess = unexpectedSuccessHandler; + request.onerror = grabEventAndContinueHandler; + event = yield undefined; + + is(event.type, "error", "Got an error event"); + is(event.target, request, "Error event targeted request"); + is(event.currentTarget, request, "Got request error first"); + is(event.currentTarget.error.name, expectedError, + "Request has correct error"); + + event = yield undefined; + + is(event.type, "error", "Got an error event"); + is(event.target, request, "Error event targeted request"); + is(event.currentTarget, transaction, "Got transaction error second"); + is(event.currentTarget.error, null, "Transaction has null error"); + + event = yield undefined; + + is(event.type, "abort", "Got an abort event"); + is(event.target, transaction, "Abort event targeted transaction"); + is(event.currentTarget, transaction, + "Abort event only targeted transaction"); + is(event.currentTarget.error.name, expectedError, + "Transaction has correct error"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_transaction_lifetimes.js b/dom/indexedDB/test/unit/test_transaction_lifetimes.js new file mode 100644 index 000000000..e42f7a218 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_lifetimes.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + event.target.transaction.onerror = errorHandler; + event.target.transaction.oncomplete = grabEventAndContinueHandler; + + let os = db.createObjectStore("foo", { autoIncrement: true }); + let index = os.createIndex("bar", "foo.bar"); + event = yield undefined; + + is(request.transaction, event.target, + "request.transaction should still be set"); + + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + is(request.transaction, null, "request.transaction should be cleared"); + + let transaction = db.transaction("foo", "readwrite"); + + os = transaction.objectStore("foo"); + // Place a request to keep the transaction alive long enough for our + // executeSoon. + let requestComplete = false; + + let wasAbleToGrabObjectStoreOutsideOfCallback = false; + let wasAbleToGrabIndexOutsideOfCallback = false; + executeSoon(function() { + ok(!requestComplete, "Ordering is correct."); + wasAbleToGrabObjectStoreOutsideOfCallback = !!transaction.objectStore("foo"); + wasAbleToGrabIndexOutsideOfCallback = + !!transaction.objectStore("foo").index("bar"); + }); + + request = os.add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + + event = yield undefined; + + requestComplete = true; + + ok(wasAbleToGrabObjectStoreOutsideOfCallback, + "Should be able to get objectStore"); + ok(wasAbleToGrabIndexOutsideOfCallback, + "Should be able to get index"); + + transaction.oncomplete = grabEventAndContinueHandler; + yield undefined; + + try { + transaction.objectStore("foo"); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got database exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + continueToNextStep(); + yield undefined; + + try { + transaction.objectStore("foo"); + ok(false, "Should have thrown!"); + } + catch (e) { + ok(e instanceof DOMException, "Got database exception."); + is(e.name, "InvalidStateError", "Good error."); + is(e.code, DOMException.INVALID_STATE_ERR, "Good error code."); + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_transaction_lifetimes_nested.js b/dom/indexedDB/test/unit/test_transaction_lifetimes_nested.js new file mode 100644 index 000000000..5d63945ba --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_lifetimes_nested.js @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "This test uses SpecialPowers"; + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; + db.createObjectStore("foo"); + yield undefined; + + let transaction1 = db.transaction("foo"); + + let transaction2; + + let comp = this.window ? SpecialPowers.wrap(Components) : Components; + let thread = comp.classes["@mozilla.org/thread-manager;1"] + .getService(comp.interfaces.nsIThreadManager) + .currentThread; + + let eventHasRun; + + thread.dispatch(function() { + eventHasRun = true; + + transaction2 = db.transaction("foo"); + }, Components.interfaces.nsIThread.DISPATCH_NORMAL); + + while (!eventHasRun) { + thread.processNextEvent(false); + } + + ok(transaction2, "Non-null transaction2"); + + continueToNextStep(); + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_transaction_ordering.js b/dom/indexedDB/test/unit/test_transaction_ordering.js new file mode 100644 index 000000000..5e3c2fd74 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_ordering.js @@ -0,0 +1,49 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + request.onsuccess = continueToNextStep; + + db.createObjectStore("foo"); + yield undefined; + + let trans1 = db.transaction("foo", "readwrite"); + let trans2 = db.transaction("foo", "readwrite"); + + let request1 = trans2.objectStore("foo").put("2", 42); + let request2 = trans1.objectStore("foo").put("1", 42); + + request1.onerror = errorHandler; + request2.onerror = errorHandler; + + trans1.oncomplete = grabEventAndContinueHandler; + trans2.oncomplete = grabEventAndContinueHandler; + + yield undefined; + yield undefined; + + let trans3 = db.transaction("foo", "readonly"); + request = trans3.objectStore("foo").get(42); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + + event = yield undefined; + is(event.target.result, "2", "Transactions were ordered properly."); + + finishTest(); + yield undefined; +} + diff --git a/dom/indexedDB/test/unit/test_unique_index_update.js b/dom/indexedDB/test/unit/test_unique_index_update.js new file mode 100644 index 000000000..97caef324 --- /dev/null +++ b/dom/indexedDB/test/unit/test_unique_index_update.js @@ -0,0 +1,64 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + let request = indexedDB.open(this.window ? window.location.pathname : "Splendid Test", 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + + let event = yield undefined; + + let db = event.target.result; + + for (let autoIncrement of [false, true]) { + let objectStore = + db.createObjectStore(autoIncrement, { keyPath: "id", + autoIncrement: autoIncrement }); + objectStore.createIndex("", "index", { unique: true }); + + for (let i = 0; i < 10; i++) { + objectStore.add({ id: i, index: i }); + } + } + + event = yield undefined; + is(event.type, "success", "expect a success event"); + + for (let autoIncrement of [false, true]) { + objectStore = db.transaction(autoIncrement, "readwrite") + .objectStore(autoIncrement); + + request = objectStore.put({ id: 5, index: 6 }); + request.onsuccess = unexpectedSuccessHandler; + request.addEventListener("error", new ExpectError("ConstraintError", true)); + event = yield undefined; + + event.preventDefault(); + + let keyRange = IDBKeyRange.only(5); + + objectStore.index("").openCursor(keyRange).onsuccess = function(event) { + let cursor = event.target.result; + ok(cursor, "Must have a cursor here"); + + is(cursor.value.index, 5, "Still have the right index value"); + + cursor.value.index = 6; + + request = cursor.update(cursor.value); + request.onsuccess = unexpectedSuccessHandler; + request.addEventListener("error", new ExpectError("ConstraintError", true)); + }; + + yield undefined; + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_view_put_get_values.js b/dom/indexedDB/test/unit/test_view_put_get_values.js new file mode 100644 index 000000000..51b9db572 --- /dev/null +++ b/dom/indexedDB/test/unit/test_view_put_get_values.js @@ -0,0 +1,102 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var disableWorkerTest = "Need a way to set temporary prefs from a worker"; + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_view_put_get_values.js"; + + const objectStoreName = "Views"; + + const viewData = { key: 1, view: getRandomView(100000) }; + + for (let external of [false, true]) { + if (external) { + info("Setting data threshold pref"); + + if (this.window) { + SpecialPowers.pushPrefEnv( + { "set": [["dom.indexedDB.dataThreshold", 0]] }, continueToNextStep); + yield undefined; + } else { + setDataThreshold(0); + } + } + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Storing view"); + + let objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + request = objectStore.add(viewData.view, viewData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + is(request.result, viewData.key, "Got correct key"); + + info("Getting view"); + + request = objectStore.get(viewData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + verifyView(request.result, viewData.view); + yield undefined; + + info("Getting view in new transaction"); + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(viewData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + verifyView(request.result, viewData.view); + yield undefined; + + getCurrentUsage(grabFileUsageAndContinueHandler); + let fileUsage = yield undefined; + + if (external) { + ok(fileUsage > 0, "File usage is not zero"); + } else { + ok(fileUsage == 0, "File usage is zero"); + } + + db.close(); + + request = indexedDB.deleteDatabase(name); + request.onerror = errorHandler; + request.onsuccess = continueToNextStepSync; + yield undefined; + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_wasm_cursors.js b/dom/indexedDB/test/unit/test_wasm_cursors.js new file mode 100644 index 000000000..08987fe46 --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_cursors.js @@ -0,0 +1,67 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_wasm_cursors.js"; + + const objectStoreName = "Wasm"; + + const wasmData = { key: 1, value: null }; + + if (!isWasmSupported()) { + finishTest(); + yield undefined; + } + + getWasmBinary('(module (func (nop)))'); + let binary = yield undefined; + wasmData.value = getWasmModule(binary); + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Storing wasm"); + + let objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + request = objectStore.add(wasmData.value, wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + is(request.result, wasmData.key, "Got correct key"); + + info("Opening cursor"); + + request = objectStore.openCursor(); + request.addEventListener("error", new ExpectError("UnknownError", true)); + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_wasm_getAll.js b/dom/indexedDB/test/unit/test_wasm_getAll.js new file mode 100644 index 000000000..23ed7b9e4 --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_getAll.js @@ -0,0 +1,136 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_wasm_getAll.js"; + + const objectStoreName = "Wasm"; + + const wasmData = [ + { key: 1, value: 42 }, + { key: 2, value: [null, null, null] }, + { key: 3, value: [null, null, null, null, null] } + ]; + + if (!isWasmSupported()) { + finishTest(); + yield undefined; + } + + getWasmBinary('(module (func (result i32) (i32.const 1)))'); + let binary = yield undefined; + wasmData[1].value[0] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 2)))'); + binary = yield undefined; + wasmData[1].value[1] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 3)))'); + binary = yield undefined; + wasmData[1].value[2] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 4)))'); + binary = yield undefined; + wasmData[2].value[0] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 5)))'); + binary = yield undefined; + wasmData[2].value[1] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 6)))'); + binary = yield undefined; + wasmData[2].value[2] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 7)))'); + binary = yield undefined; + wasmData[2].value[3] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 8)))'); + binary = yield undefined; + wasmData[2].value[4] = getWasmModule(binary); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Storing values"); + + let objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + let addedCount = 0; + for (let i in wasmData) { + request = objectStore.add(wasmData[i].value, wasmData[i].key); + request.onsuccess = function(event) { + if (++addedCount == wasmData.length) { + continueToNextStep(); + } + } + } + yield undefined; + + info("Getting values"); + + request = db.transaction(objectStoreName) + .objectStore(objectStoreName) + .getAll(); + request.onsuccess = continueToNextStepSync; + yield undefined; + + info("Verifying values"); + + // Can't call yield inside of the verify function. + let modulesToProcess = []; + + function verifyArray(array1, array2) { + is(array1 instanceof Array, true, "Got an array object"); + is(array1.length, array2.length, "Same length"); + } + + function verifyData(data1, data2) { + if (data2 instanceof Array) { + verifyArray(data1, data2); + for (let i in data2) { + verifyData(data1[i], data2[i]); + } + } else if (data2 instanceof WebAssembly.Module) { + modulesToProcess.push({ module1: data1, module2: data2 }); + } else { + is(data1, data2, "Same value"); + } + } + + verifyArray(request.result, wasmData); + for (let i in wasmData) { + verifyData(request.result[i], wasmData[i].value); + } + + for (let moduleToProcess of modulesToProcess) { + verifyWasmModule(moduleToProcess.module1, moduleToProcess.module2); + yield undefined; + } + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_wasm_index_getAllObjects.js b/dom/indexedDB/test/unit/test_wasm_index_getAllObjects.js new file mode 100644 index 000000000..f03e79e5c --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_index_getAllObjects.js @@ -0,0 +1,111 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_wasm_getAll.js"; + + const objectStoreName = "Wasm"; + + const wasmData = [ + { key: 1, value: { name: "foo1", data: 42 } }, + { key: 2, value: { name: "foo2", data: [null, null, null] } }, + { key: 3, value: { name: "foo3", data: [null, null, null, null, null] } } + ]; + + const indexData = { name: "nameIndex", keyPath: "name", options: { } }; + + if (!isWasmSupported()) { + finishTest(); + yield undefined; + } + + getWasmBinary('(module (func (result i32) (i32.const 1)))'); + let binary = yield undefined; + wasmData[1].value.data[0] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 2)))'); + binary = yield undefined; + wasmData[1].value.data[1] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 3)))'); + binary = yield undefined; + wasmData[1].value.data[2] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 4)))'); + binary = yield undefined; + wasmData[2].value.data[0] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 5)))'); + binary = yield undefined; + wasmData[2].value.data[1] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 6)))'); + binary = yield undefined; + wasmData[2].value.data[2] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 7)))'); + binary = yield undefined; + wasmData[2].value.data[3] = getWasmModule(binary); + + getWasmBinary('(module (func (result i32) (i32.const 8)))'); + binary = yield undefined; + wasmData[2].value.data[4] = getWasmModule(binary); + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + let objectStore = request.result.createObjectStore(objectStoreName); + + info("Creating index"); + + objectStore.createIndex(indexData.name, indexData.keyPath, indexData.options); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Storing values"); + + objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + let addedCount = 0; + for (let i in wasmData) { + request = objectStore.add(wasmData[i].value, wasmData[i].key); + request.onsuccess = function(event) { + if (++addedCount == wasmData.length) { + continueToNextStep(); + } + } + } + yield undefined; + + info("Getting values"); + + request = db.transaction(objectStoreName) + .objectStore(objectStoreName) + .index("nameIndex") + .getAll(); + request.addEventListener("error", new ExpectError("UnknownError", true)); + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_wasm_indexes.js b/dom/indexedDB/test/unit/test_wasm_indexes.js new file mode 100644 index 000000000..467e0c297 --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_indexes.js @@ -0,0 +1,80 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_wasm_indexes.js"; + + const objectStoreName = "Wasm"; + + const wasmData = { key: 1, value: { name: "foo", data: null } }; + + const indexData = { name: "nameIndex", keyPath: "name", options: { } }; + + if (!isWasmSupported()) { + finishTest(); + yield undefined; + } + + getWasmBinary('(module (func (nop)))'); + let binary = yield undefined; + wasmData.value.data = getWasmModule(binary); + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + let objectStore = request.result.createObjectStore(objectStoreName); + + info("Creating index"); + + objectStore.createIndex(indexData.name, indexData.keyPath, indexData.options); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Storing wasm"); + + objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + request = objectStore.add(wasmData.value, wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + is(request.result, wasmData.key, "Got correct key"); + + info("Getting wasm"); + + request = objectStore.index("nameIndex").get("foo"); + request.addEventListener("error", new ExpectError("UnknownError", true)); + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + info("Opening cursor"); + + request = objectStore.index("nameIndex").openCursor(); + request.addEventListener("error", new ExpectError("UnknownError", true)); + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_wasm_put_get_values.js b/dom/indexedDB/test/unit/test_wasm_put_get_values.js new file mode 100644 index 000000000..fb8827009 --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_put_get_values.js @@ -0,0 +1,83 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = + this.window ? window.location.pathname : "test_wasm_put_get_values.js"; + + const objectStoreName = "Wasm"; + + const wasmData = { key: 1, value: null }; + + if (!isWasmSupported()) { + finishTest(); + yield undefined; + } + + getWasmBinary('(module (func (nop)))'); + let binary = yield undefined; + wasmData.value = getWasmModule(binary); + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + // upgradeneeded + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + + info("Creating objectStore"); + + request.result.createObjectStore(objectStoreName); + + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Storing wasm"); + + let objectStore = db.transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + request = objectStore.add(wasmData.value, wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + is(request.result, wasmData.key, "Got correct key"); + + info("Getting wasm"); + + request = objectStore.get(wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + info("Verifying wasm"); + + verifyWasmModule(request.result, wasmData.value); + yield undefined; + + info("Getting wasm in new transaction"); + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + info("Verifying wasm"); + + verifyWasmModule(request.result, wasmData.value); + yield undefined; + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_wasm_recompile.js b/dom/indexedDB/test/unit/test_wasm_recompile.js new file mode 100644 index 000000000..512dc3eff --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_recompile.js @@ -0,0 +1,124 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = "test_wasm_recompile.js"; + + const objectStoreName = "Wasm"; + + const wasmData = { key: 1, wasm: null }; + + // The goal of this test is to prove that wasm is recompiled and the on-disk + // copy updated. + + if (!isWasmSupported()) { + finishTest(); + yield undefined; + } + + getWasmBinary('(module (func (nop)))'); + let binary = yield undefined; + + wasmData.wasm = getWasmModule(binary); + + info("Installing profile"); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + // The profile was created by an older build (buildId: 20161116145318, + // cpuId: X64=0x2). It contains one stored wasm module (file id 1 - bytecode + // and file id 2 - compiled/machine code). The file create_db.js in the + // package was run locally (specifically it was temporarily added to + // xpcshell-parent-process.ini and then executed: + // mach xpcshell-test dom/indexedDB/test/unit/create_db.js + installPackagedProfile("wasm_recompile_profile"); + + let filesDir = getChromeFilesDir(); + + let file = filesDir.clone(); + file.append("2"); + + info("Reading out contents of compiled blob"); + + let fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(File.createFromNsIFile(file)); + + yield undefined; + + let compiledBuffer = fileReader.result; + + info("Opening database"); + + let request = indexedDB.open(name); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = continueToNextStepSync; + yield undefined; + + // success + let db = request.result; + db.onerror = errorHandler; + + info("Getting wasm"); + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + info("Verifying wasm module"); + + verifyWasmModule(request.result, wasmData.wasm); + yield undefined; + + info("Reading out contents of new compiled blob"); + + fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(File.createFromNsIFile(file)); + + yield undefined; + + let newCompiledBuffer = fileReader.result; + + info("Verifying blobs differ"); + + ok(!compareBuffers(newCompiledBuffer, compiledBuffer), "Blobs differ"); + + info("Getting wasm again"); + + request = db.transaction([objectStoreName]) + .objectStore(objectStoreName).get(wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + info("Verifying wasm module"); + + verifyWasmModule(request.result, wasmData.wasm); + yield undefined; + + info("Reading contents of new compiled blob again"); + + fileReader = new FileReader(); + fileReader.onload = continueToNextStepSync; + fileReader.readAsArrayBuffer(File.createFromNsIFile(file)); + + yield undefined; + + let newCompiledBuffer2 = fileReader.result; + + info("Verifying blob didn't change"); + + ok(compareBuffers(newCompiledBuffer2, newCompiledBuffer), + "Blob didn't change"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/test_writer_starvation.js b/dom/indexedDB/test/unit/test_writer_starvation.js new file mode 100644 index 000000000..141bd1d93 --- /dev/null +++ b/dom/indexedDB/test/unit/test_writer_starvation.js @@ -0,0 +1,104 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +if (!this.window) { + this.runTest = function() { + todo(false, "Test disabled in xpcshell test suite for now"); + finishTest(); + } +} + +var testGenerator = testSteps(); + +function testSteps() +{ + const name = this.window ? window.location.pathname : "Splendid Test"; + + // Needs to be enough to saturate the thread pool. + const SYNC_REQUEST_COUNT = 25; + + let request = indexedDB.open(name, 1); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + let db = event.target.result; + db.onerror = errorHandler; + + is(event.target.transaction.mode, "versionchange", "Correct mode"); + + let objectStore = db.createObjectStore("foo", { autoIncrement: true }); + + request = objectStore.add({}); + request.onerror = errorHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + let key = event.target.result; + ok(key, "Got a key"); + + yield undefined; + + let continueReading = true; + let readerCount = 0; + let writerCount = 0; + let callbackCount = 0; + + // Generate a bunch of reads right away without returning to the event + // loop. + info("Generating " + SYNC_REQUEST_COUNT + " readonly requests"); + + for (let i = 0; i < SYNC_REQUEST_COUNT; i++) { + readerCount++; + let request = db.transaction("foo").objectStore("foo").get(key); + request.onsuccess = function(event) { + is(event.target.transaction.mode, "readonly", "Correct mode"); + callbackCount++; + }; + } + + while (continueReading) { + readerCount++; + info("Generating additional readonly request (" + readerCount + ")"); + let request = db.transaction("foo").objectStore("foo").get(key); + request.onsuccess = function(event) { + callbackCount++; + info("Received readonly request callback (" + callbackCount + ")"); + is(event.target.transaction.mode, "readonly", "Correct mode"); + if (callbackCount == SYNC_REQUEST_COUNT) { + writerCount++; + info("Generating 1 readwrite request with " + readerCount + + " previous readonly requests"); + let request = db.transaction("foo", "readwrite") + .objectStore("foo") + .add({}, readerCount); + request.onsuccess = function(event) { + callbackCount++; + info("Received readwrite request callback (" + callbackCount + ")"); + is(event.target.transaction.mode, "readwrite", "Correct mode"); + is(event.target.result, callbackCount, + "write callback came before later reads"); + } + } + else if (callbackCount == SYNC_REQUEST_COUNT + 5) { + continueReading = false; + } + }; + + setTimeout(function() { testGenerator.next(); }, writerCount ? 1000 : 100); + yield undefined; + } + + while (callbackCount < (readerCount + writerCount)) { + executeSoon(function() { testGenerator.next(); }); + yield undefined; + } + + is(callbackCount, readerCount + writerCount, "All requests accounted for"); + + finishTest(); + yield undefined; +} diff --git a/dom/indexedDB/test/unit/wasm_recompile_profile.zip b/dom/indexedDB/test/unit/wasm_recompile_profile.zip Binary files differnew file mode 100644 index 000000000..50ca3ef89 --- /dev/null +++ b/dom/indexedDB/test/unit/wasm_recompile_profile.zip diff --git a/dom/indexedDB/test/unit/xpcshell-child-process.ini b/dom/indexedDB/test/unit/xpcshell-child-process.ini new file mode 100644 index 000000000..970fe8c3d --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-child-process.ini @@ -0,0 +1,19 @@ +# 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/. + +[DEFAULT] +dupe-manifest = +head = xpcshell-head-child-process.js +tail = +skip-if = toolkit == 'android' || toolkit == 'gonk' +support-files = + GlobalObjectsChild.js + GlobalObjectsComponent.js + GlobalObjectsComponent.manifest + GlobalObjectsModule.jsm + GlobalObjectsSandbox.js + xpcshell-head-parent-process.js + xpcshell-shared.ini + +[include:xpcshell-shared.ini] diff --git a/dom/indexedDB/test/unit/xpcshell-head-child-process.js b/dom/indexedDB/test/unit/xpcshell-head-child-process.js new file mode 100644 index 000000000..2e704f8dc --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-head-child-process.js @@ -0,0 +1,27 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function run_test() { + const { 'classes': Cc, 'interfaces': Ci, 'utils': Cu } = Components; + + const INDEXEDDB_HEAD_FILE = "xpcshell-head-parent-process.js"; + const INDEXEDDB_PREF_EXPERIMENTAL = "dom.indexedDB.experimental"; + + // IndexedDB needs a profile. + do_get_profile(); + + let thisTest = _TEST_FILE.toString().replace(/\\/g, "/"); + thisTest = thisTest.substring(thisTest.lastIndexOf("/") + 1); + + _HEAD_FILES.push(do_get_file(INDEXEDDB_HEAD_FILE).path.replace(/\\/g, "/")); + + + let prefs = + Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService) + .getBranch(null); + prefs.setBoolPref(INDEXEDDB_PREF_EXPERIMENTAL, true); + + run_test_in_child(thisTest); +} diff --git a/dom/indexedDB/test/unit/xpcshell-head-parent-process.js b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js new file mode 100644 index 000000000..def791f52 --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js @@ -0,0 +1,700 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var { 'classes': Cc, 'interfaces': Ci, 'utils': Cu } = Components; + +if (!("self" in this)) { + this.self = this; +} + +const DOMException = Ci.nsIDOMDOMException; + +var bufferCache = []; + +function is(a, b, msg) { + do_check_eq(a, b, Components.stack.caller); +} + +function ok(cond, msg) { + do_check_true(!!cond, Components.stack.caller); +} + +function isnot(a, b, msg) { + do_check_neq(a, b, Components.stack.caller); +} + +function executeSoon(fun) { + do_execute_soon(fun); +} + +function todo(condition, name, diag) { + todo_check_true(condition, Components.stack.caller); +} + +function info(name, message) { + do_print(name); +} + +function run_test() { + runTest(); +}; + +if (!this.runTest) { + this.runTest = function() + { + if (SpecialPowers.isMainProcess()) { + // XPCShell does not get a profile by default. + do_get_profile(); + + enableTesting(); + enableExperimental(); + enableWasm(); + } + + Cu.importGlobalProperties(["indexedDB", "Blob", "File", "FileReader"]); + + do_test_pending(); + testGenerator.next(); + } +} + +function finishTest() +{ + if (SpecialPowers.isMainProcess()) { + resetWasm(); + resetExperimental(); + resetTesting(); + + SpecialPowers.notifyObserversInParentProcess(null, "disk-space-watcher", + "free"); + } + + SpecialPowers.removeFiles(); + + do_execute_soon(function(){ + testGenerator.close(); + do_test_finished(); + }) +} + +function grabEventAndContinueHandler(event) +{ + testGenerator.send(event); +} + +function continueToNextStep() +{ + do_execute_soon(function() { + testGenerator.next(); + }); +} + +function errorHandler(event) +{ + try { + dump("indexedDB error: " + event.target.error.name); + } catch(e) { + dump("indexedDB error: " + e); + } + do_check_true(false); + finishTest(); +} + +function unexpectedSuccessHandler() +{ + do_check_true(false); + finishTest(); +} + +function expectedErrorHandler(name) +{ + return function(event) { + do_check_eq(event.type, "error"); + do_check_eq(event.target.error.name, name); + event.preventDefault(); + grabEventAndContinueHandler(event); + }; +} + +function expectUncaughtException(expecting) +{ + // This is dummy for xpcshell test. +} + +function ExpectError(name, preventDefault) +{ + this._name = name; + this._preventDefault = preventDefault; +} +ExpectError.prototype = { + handleEvent: function(event) + { + do_check_eq(event.type, "error"); + do_check_eq(this._name, event.target.error.name); + if (this._preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } + grabEventAndContinueHandler(event); + } +}; + +function continueToNextStepSync() +{ + testGenerator.next(); +} + +function compareKeys(k1, k2) { + let t = typeof k1; + if (t != typeof k2) + return false; + + if (t !== "object") + return k1 === k2; + + if (k1 instanceof Date) { + return (k2 instanceof Date) && + k1.getTime() === k2.getTime(); + } + + if (k1 instanceof Array) { + if (!(k2 instanceof Array) || + k1.length != k2.length) + return false; + + for (let i = 0; i < k1.length; ++i) { + if (!compareKeys(k1[i], k2[i])) + return false; + } + + return true; + } + + return false; +} + +function addPermission(permission, url) +{ + throw "addPermission"; +} + +function removePermission(permission, url) +{ + throw "removePermission"; +} + +function allowIndexedDB(url) +{ + throw "allowIndexedDB"; +} + +function disallowIndexedDB(url) +{ + throw "disallowIndexedDB"; +} + +function enableExperimental() +{ + SpecialPowers.setBoolPref("dom.indexedDB.experimental", true); +} + +function resetExperimental() +{ + SpecialPowers.clearUserPref("dom.indexedDB.experimental"); +} + +function enableTesting() +{ + SpecialPowers.setBoolPref("dom.indexedDB.testing", true); +} + +function resetTesting() +{ + SpecialPowers.clearUserPref("dom.indexedDB.testing"); +} + +function enableWasm() +{ + SpecialPowers.setBoolPref("javascript.options.wasm", true); +} + +function resetWasm() +{ + SpecialPowers.clearUserPref("javascript.options.wasm"); +} + +function gc() +{ + Cu.forceGC(); + Cu.forceCC(); +} + +function scheduleGC() +{ + SpecialPowers.exactGC(continueToNextStep); +} + +function setTimeout(fun, timeout) { + let timer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + var event = { + notify: function (timer) { + fun(); + } + }; + timer.initWithCallback(event, timeout, + Components.interfaces.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +function resetOrClearAllDatabases(callback, clear) { + if (!SpecialPowers.isMainProcess()) { + throw new Error("clearAllDatabases not implemented for child processes!"); + } + + let quotaManagerService = Cc["@mozilla.org/dom/quota-manager-service;1"] + .getService(Ci.nsIQuotaManagerService); + + const quotaPref = "dom.quotaManager.testing"; + + let oldPrefValue; + if (SpecialPowers._getPrefs().prefHasUserValue(quotaPref)) { + oldPrefValue = SpecialPowers.getBoolPref(quotaPref); + } + + SpecialPowers.setBoolPref(quotaPref, true); + + let request; + + try { + if (clear) { + request = quotaManagerService.clear(); + } else { + request = quotaManagerService.reset(); + } + } catch(e) { + if (oldPrefValue !== undefined) { + SpecialPowers.setBoolPref(quotaPref, oldPrefValue); + } else { + SpecialPowers.clearUserPref(quotaPref); + } + throw e; + } + + request.callback = callback; +} + +function resetAllDatabases(callback) { + resetOrClearAllDatabases(callback, false); +} + +function clearAllDatabases(callback) { + resetOrClearAllDatabases(callback, true); +} + +function installPackagedProfile(packageName) +{ + let directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + + let profileDir = directoryService.get("ProfD", Ci.nsIFile); + + let currentDir = directoryService.get("CurWorkD", Ci.nsIFile); + + let packageFile = currentDir.clone(); + packageFile.append(packageName + ".zip"); + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] + .createInstance(Ci.nsIZipReader); + zipReader.open(packageFile); + + let entryNames = []; + let entries = zipReader.findEntries(null); + while (entries.hasMore()) { + let entry = entries.getNext(); + if (entry != "create_db.html") { + entryNames.push(entry); + } + } + entryNames.sort(); + + for (let entryName of entryNames) { + let zipentry = zipReader.getEntry(entryName); + + let file = profileDir.clone(); + let split = entryName.split("/"); + for(let i = 0; i < split.length; i++) { + file.append(split[i]); + } + + if (zipentry.isDirectory) { + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } else { + let istream = zipReader.getInputStream(entryName); + + var ostream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, parseInt("0644", 8), 0); + + let bostream = Cc['@mozilla.org/network/buffered-output-stream;1'] + .createInstance(Ci.nsIBufferedOutputStream); + bostream.init(ostream, 32768); + + bostream.writeFrom(istream, istream.available()); + + istream.close(); + bostream.close(); + } + } + + zipReader.close(); +} + +function getChromeFilesDir() +{ + let dirService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + + let profileDir = dirService.get("ProfD", Ci.nsIFile); + + let idbDir = profileDir.clone(); + idbDir.append("storage"); + idbDir.append("permanent"); + idbDir.append("chrome"); + idbDir.append("idb"); + + let idbEntries = idbDir.directoryEntries; + while (idbEntries.hasMoreElements()) { + let entry = idbEntries.getNext(); + let file = entry.QueryInterface(Ci.nsIFile); + if (file.isDirectory()) { + return file; + } + } + + throw new Error("files directory doesn't exist!"); +} + +function getView(size) +{ + let buffer = new ArrayBuffer(size); + let view = new Uint8Array(buffer); + is(buffer.byteLength, size, "Correct byte length"); + return view; +} + +function getRandomView(size) +{ + let view = getView(size); + for (let i = 0; i < size; i++) { + view[i] = parseInt(Math.random() * 255) + } + return view; +} + +function getBlob(str) +{ + return new Blob([str], {type: "type/text"}); +} + +function getFile(name, type, str) +{ + return new File([str], name, {type: type}); +} + +function isWasmSupported() +{ + let testingFunctions = Cu.getJSTestingFunctions(); + return testingFunctions.wasmIsSupported(); +} + +function getWasmBinarySync(text) +{ + let testingFunctions = Cu.getJSTestingFunctions(); + let binary = testingFunctions.wasmTextToBinary(text); + return binary; +} + +function getWasmBinary(text) +{ + let binary = getWasmBinarySync(text); + executeSoon(function() { + testGenerator.send(binary); + }); +} + +function getWasmModule(binary) +{ + let module = new WebAssembly.Module(binary); + return module; +} + +function compareBuffers(buffer1, buffer2) +{ + if (buffer1.byteLength != buffer2.byteLength) { + return false; + } + + let view1 = buffer1 instanceof Uint8Array ? buffer1 : new Uint8Array(buffer1); + let view2 = buffer2 instanceof Uint8Array ? buffer2 : new Uint8Array(buffer2); + for (let i = 0; i < buffer1.byteLength; i++) { + if (view1[i] != view2[i]) { + return false; + } + } + return true; +} + +function verifyBuffers(buffer1, buffer2) +{ + ok(compareBuffers(buffer1, buffer2), "Correct buffer data"); +} + +function verifyBlob(blob1, blob2) +{ + is(blob1 instanceof Components.interfaces.nsIDOMBlob, true, + "Instance of nsIDOMBlob"); + is(blob1 instanceof File, blob2 instanceof File, + "Instance of DOM File"); + is(blob1.size, blob2.size, "Correct size"); + is(blob1.type, blob2.type, "Correct type"); + if (blob2 instanceof File) { + is(blob1.name, blob2.name, "Correct name"); + } + + let buffer1; + let buffer2; + + for (let i = 0; i < bufferCache.length; i++) { + if (bufferCache[i].blob == blob2) { + buffer2 = bufferCache[i].buffer; + break; + } + } + + if (!buffer2) { + let reader = new FileReader(); + reader.readAsArrayBuffer(blob2); + reader.onload = function(event) { + buffer2 = event.target.result; + bufferCache.push({ blob: blob2, buffer: buffer2 }); + if (buffer1) { + verifyBuffers(buffer1, buffer2); + testGenerator.next(); + } + } + } + + let reader = new FileReader(); + reader.readAsArrayBuffer(blob1); + reader.onload = function(event) { + buffer1 = event.target.result; + if (buffer2) { + verifyBuffers(buffer1, buffer2); + testGenerator.next(); + } + } +} + +function verifyMutableFile(mutableFile1, file2) +{ + is(mutableFile1 instanceof IDBMutableFile, true, + "Instance of IDBMutableFile"); + is(mutableFile1.name, file2.name, "Correct name"); + is(mutableFile1.type, file2.type, "Correct type"); + continueToNextStep(); +} + +function verifyView(view1, view2) +{ + is(view1.byteLength, view2.byteLength, "Correct byteLength"); + verifyBuffers(view1, view2); + continueToNextStep(); +} + +function verifyWasmModule(module1, module2) +{ + let testingFunctions = Cu.getJSTestingFunctions(); + let exp1 = testingFunctions.wasmExtractCode(module1); + let exp2 = testingFunctions.wasmExtractCode(module2); + let code1 = exp1.code; + let code2 = exp2.code; + ok(code1 instanceof Uint8Array, "Instance of Uint8Array"); + ok(code1.length == code2.length, "Correct length"); + verifyBuffers(code1, code2); + continueToNextStep(); +} + +function grabResultAndContinueHandler(request) +{ + testGenerator.send(request.result); +} + +function grabFileUsageAndContinueHandler(request) +{ + testGenerator.send(request.result.fileUsage); +} + +function getUsage(usageHandler, getAll) +{ + let qms = Cc["@mozilla.org/dom/quota-manager-service;1"] + .getService(Ci.nsIQuotaManagerService); + qms.getUsage(usageHandler, getAll) +} + +function getCurrentUsage(usageHandler) +{ + let qms = Cc["@mozilla.org/dom/quota-manager-service;1"] + .getService(Ci.nsIQuotaManagerService); + let principal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + qms.getUsageForPrincipal(principal, usageHandler); +} + +function setTemporaryStorageLimit(limit) +{ + const pref = "dom.quotaManager.temporaryStorage.fixedLimit"; + if (limit) { + info("Setting temporary storage limit to " + limit); + SpecialPowers.setIntPref(pref, limit); + } else { + info("Removing temporary storage limit"); + SpecialPowers.clearUserPref(pref); + } +} + +function setDataThreshold(threshold) +{ + info("Setting data threshold to " + threshold); + SpecialPowers.setIntPref("dom.indexedDB.dataThreshold", threshold); +} + +function setMaxSerializedMsgSize(aSize) +{ + info("Setting maximal size of a serialized message to " + aSize); + SpecialPowers.setIntPref("dom.indexedDB.maxSerializedMsgSize", aSize); +} + +function getPrincipal(url) +{ + let uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(url, null, null); + let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + return ssm.createCodebasePrincipal(uri, {}); +} + +var SpecialPowers = { + isMainProcess: function() { + return Components.classes["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULRuntime) + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + }, + notifyObservers: function(subject, topic, data) { + var obsvc = Cc['@mozilla.org/observer-service;1'] + .getService(Ci.nsIObserverService); + obsvc.notifyObservers(subject, topic, data); + }, + notifyObserversInParentProcess: function(subject, topic, data) { + if (subject) { + throw new Error("Can't send subject to another process!"); + } + return this.notifyObservers(subject, topic, data); + }, + getBoolPref: function(prefName) { + return this._getPrefs().getBoolPref(prefName); + }, + setBoolPref: function(prefName, value) { + this._getPrefs().setBoolPref(prefName, value); + }, + setIntPref: function(prefName, value) { + this._getPrefs().setIntPref(prefName, value); + }, + clearUserPref: function(prefName) { + this._getPrefs().clearUserPref(prefName); + }, + // Copied (and slightly adjusted) from specialpowersAPI.js + exactGC: function(callback) { + let count = 0; + + function doPreciseGCandCC() { + function scheduledGCCallback() { + Components.utils.forceCC(); + + if (++count < 2) { + doPreciseGCandCC(); + } else { + callback(); + } + } + + Components.utils.schedulePreciseGC(scheduledGCCallback); + } + + doPreciseGCandCC(); + }, + + _getPrefs: function() { + var prefService = + Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService); + return prefService.getBranch(null); + }, + + get Cc() { + return Cc; + }, + + get Ci() { + return Ci; + }, + + get Cu() { + return Cu; + }, + + // Based on SpecialPowersObserver.prototype.receiveMessage + createFiles: function(requests, callback) { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); + let filePaths = new Array; + if (!this._createdFiles) { + this._createdFiles = new Array; + } + let createdFiles = this._createdFiles; + requests.forEach(function(request) { + const filePerms = 0o666; + let testFile = dirSvc.get("ProfD", Ci.nsIFile); + if (request.name) { + testFile.append(request.name); + } else { + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms); + } + let outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE + filePerms, 0); + if (request.data) { + outStream.write(request.data, request.data.length); + outStream.close(); + } + filePaths.push(File.createFromFileName(testFile.path, request.options)); + createdFiles.push(testFile); + }); + + setTimeout(function () { + callback(filePaths); + }, 0); + }, + + removeFiles: function() { + if (this._createdFiles) { + this._createdFiles.forEach(function (testFile) { + try { + testFile.remove(false); + } catch (e) {} + }); + this._createdFiles = null; + } + }, +}; diff --git a/dom/indexedDB/test/unit/xpcshell-parent-process.ini b/dom/indexedDB/test/unit/xpcshell-parent-process.ini new file mode 100644 index 000000000..22bc861cc --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-parent-process.ini @@ -0,0 +1,72 @@ +# 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/. + +[DEFAULT] +dupe-manifest = +head = xpcshell-head-parent-process.js +tail = +skip-if = toolkit == 'gonk' +support-files = + bug1056939_profile.zip + defaultStorageUpgrade_profile.zip + idbSubdirUpgrade1_profile.zip + idbSubdirUpgrade2_profile.zip + mutableFileUpgrade_profile.zip + oldDirectories_profile.zip + GlobalObjectsChild.js + GlobalObjectsComponent.js + GlobalObjectsComponent.manifest + GlobalObjectsModule.jsm + GlobalObjectsSandbox.js + getUsage_profile.zip + metadata2Restore_profile.zip + metadataRestore_profile.zip + schema18upgrade_profile.zip + schema21upgrade_profile.zip + schema23upgrade_profile.zip + snappyUpgrade_profile.zip + storagePersistentUpgrade_profile.zip + wasm_recompile_profile.zip + xpcshell-shared.ini + +[include:xpcshell-shared.ini] + +[test_blob_file_backed.js] +[test_bug1056939.js] +[test_cleanup_transaction.js] +[test_database_close_without_onclose.js] +[test_database_onclose.js] +[test_defaultStorageUpgrade.js] +[test_file_copy_failure.js] +[test_getUsage.js] +[test_idbSubdirUpgrade.js] +[test_globalObjects_ipc.js] +skip-if = toolkit == 'android' +[test_idle_maintenance.js] +[test_invalidate.js] +# disabled for the moment. +skip-if = true +[test_lowDiskSpace.js] +[test_maximal_serialized_object_size.js] +[test_metadata2Restore.js] +[test_metadataRestore.js] +[test_mutableFileUpgrade.js] +[test_oldDirectories.js] +[test_quotaExceeded_recovery.js] +[test_readwriteflush_disabled.js] +[test_schema18upgrade.js] +[test_schema21upgrade.js] +[test_schema23upgrade.js] +[test_snappyUpgrade.js] +[test_storagePersistentUpgrade.js] +[test_temporary_storage.js] +# bug 951017: intermittent failure on Android x86 emulator +skip-if = os == "android" && processor == "x86" +[test_view_put_get_values.js] +[test_wasm_cursors.js] +[test_wasm_getAll.js] +[test_wasm_index_getAllObjects.js] +[test_wasm_indexes.js] +[test_wasm_put_get_values.js] +[test_wasm_recompile.js] diff --git a/dom/indexedDB/test/unit/xpcshell-shared.ini b/dom/indexedDB/test/unit/xpcshell-shared.ini new file mode 100644 index 000000000..05359097b --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-shared.ini @@ -0,0 +1,96 @@ +# 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/. + +[test_abort_deleted_index.js] +[test_abort_deleted_objectStore.js] +[test_add_put.js] +[test_add_twice_failure.js] +[test_advance.js] +[test_autoIncrement.js] +[test_autoIncrement_indexes.js] +[test_blocked_order.js] +[test_clear.js] +[test_complex_keyPaths.js] +[test_count.js] +[test_create_index.js] +[test_create_index_with_integer_keys.js] +[test_create_locale_aware_index.js] +skip-if = toolkit == 'android' # bug 864843 +[test_create_objectStore.js] +[test_cursor_cycle.js] +[test_cursor_mutation.js] +[test_cursor_update_updates_indexes.js] +[test_cursors.js] +[test_deleteDatabase.js] +[test_deleteDatabase_interactions.js] +[test_deleteDatabase_onblocked.js] +[test_deleteDatabase_onblocked_duringVersionChange.js] +[test_event_source.js] +[test_getAll.js] +[test_globalObjects_other.js] +skip-if = toolkit == 'android' # bug 1079278 +[test_globalObjects_xpc.js] +[test_global_data.js] +[test_index_empty_keyPath.js] +[test_index_getAll.js] +[test_index_getAllObjects.js] +[test_index_object_cursors.js] +[test_index_update_delete.js] +[test_indexes.js] +[test_indexes_bad_values.js] +[test_indexes_funny_things.js] +[test_invalid_cursor.js] +[test_invalid_version.js] +[test_key_requirements.js] +[test_keys.js] +[test_locale_aware_indexes.js] +skip-if = toolkit == 'android' # bug 864843 +[test_locale_aware_index_getAll.js] +skip-if = toolkit == 'android' # bug 864843 +[test_locale_aware_index_getAllObjects.js] +skip-if = toolkit == 'android' # bug 864843 +[test_multientry.js] +[test_names_sorted.js] +[test_object_identity.js] +[test_objectCursors.js] +[test_objectStore_getAllKeys.js] +[test_objectStore_inline_autoincrement_key_added_on_put.js] +[test_objectStore_openKeyCursor.js] +[test_objectStore_remove_values.js] +[test_odd_result_order.js] +[test_open_empty_db.js] +[test_open_for_principal.js] +[test_open_objectStore.js] +[test_optionalArguments.js] +[test_overlapping_transactions.js] +[test_persistenceType.js] +[test_put_get_values.js] +[test_put_get_values_autoIncrement.js] +[test_readonly_transactions.js] +[test_remove_index.js] +[test_rename_index.js] +[test_rename_index_errors.js] +[test_remove_objectStore.js] +[test_rename_objectStore.js] +[test_rename_objectStore_errors.js] +[test_request_readyState.js] +[test_sandbox.js] +[test_setVersion.js] +[test_setVersion_abort.js] +[test_setVersion_events.js] +[test_setVersion_exclusion.js] +[test_setVersion_throw.js] +[test_success_events_after_abort.js] +[test_table_locks.js] +[test_table_rollback.js] +[test_traffic_jam.js] +[test_transaction_abort.js] +[test_transaction_abort_hang.js] +[test_transaction_duplicate_store_names.js] +[test_transaction_error.js] +[test_transaction_lifetimes.js] +[test_transaction_lifetimes_nested.js] +[test_transaction_ordering.js] +[test_unique_index_update.js] +[test_writer_starvation.js] |