diff options
Diffstat (limited to 'dom/indexedDB')
388 files changed, 75499 insertions, 0 deletions
diff --git a/dom/indexedDB/ActorsChild.cpp b/dom/indexedDB/ActorsChild.cpp new file mode 100644 index 0000000000..00d1f63b01 --- /dev/null +++ b/dom/indexedDB/ActorsChild.cpp @@ -0,0 +1,2859 @@ +/* -*- 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 <type_traits> + +#include "ActorsChild.h" +#include "BackgroundChildImpl.h" +#include "IDBDatabase.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBObjectStore.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDBCommon.h" +#include "js/Array.h" // JS::NewArrayObject, JS::SetArrayLength +#include "js/Date.h" // JS::NewDateObject, JS::TimeClip +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty +#include <mozIRemoteLazyInputStream.h> +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/Maybe.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileChild.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/Encoding.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/TaskQueue.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIAsyncInputStream.h" +#include "nsIEventTarget.h" +#include "nsIFileStreams.h" +#include "nsNetCID.h" +#include "nsPIDOMWindow.h" +#include "nsThreadUtils.h" +#include "nsTraceRefcnt.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "ThreadLocal.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::indexedDB { + +/******************************************************************************* + * ThreadLocal + ******************************************************************************/ + +ThreadLocal::ThreadLocal(const nsID& aBackgroundChildLoggingId) + : mLoggingInfo(aBackgroundChildLoggingId, 1, -1, 1), + mLoggingIdString(aBackgroundChildLoggingId) { + MOZ_COUNT_CTOR(mozilla::dom::indexedDB::ThreadLocal); +} + +ThreadLocal::~ThreadLocal() { + MOZ_COUNT_DTOR(mozilla::dom::indexedDB::ThreadLocal); +} + +/******************************************************************************* + * 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(CCReason::API); +#endif // BUILD_GC_ON_IPC_MESSAGES +} + +class MOZ_STACK_CLASS AutoSetCurrentTransaction final { + using BackgroundChildImpl = mozilla::ipc::BackgroundChildImpl; + + Maybe<IDBTransaction&> const mTransaction; + Maybe<IDBTransaction&> mPreviousTransaction; + ThreadLocal* mThreadLocal; + + public: + AutoSetCurrentTransaction(const AutoSetCurrentTransaction&) = delete; + AutoSetCurrentTransaction(AutoSetCurrentTransaction&&) = delete; + AutoSetCurrentTransaction& operator=(const AutoSetCurrentTransaction&) = + delete; + AutoSetCurrentTransaction& operator=(AutoSetCurrentTransaction&&) = delete; + + explicit AutoSetCurrentTransaction(Maybe<IDBTransaction&> aTransaction) + : mTransaction(aTransaction), + mPreviousTransaction(), + mThreadLocal(nullptr) { + if (aTransaction) { + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + // Hang onto this for resetting later. + mThreadLocal = threadLocal->mIndexedDBThreadLocal.get(); + MOZ_ASSERT(mThreadLocal); + + // Save the current value. + mPreviousTransaction = mThreadLocal->MaybeCurrentTransactionRef(); + + // Set the new value. + mThreadLocal->SetCurrentTransaction(aTransaction); + } + } + + ~AutoSetCurrentTransaction() { + MOZ_ASSERT_IF(mThreadLocal, mTransaction); + MOZ_ASSERT_IF(mThreadLocal, + ReferenceEquals(mThreadLocal->MaybeCurrentTransactionRef(), + mTransaction)); + + if (mThreadLocal) { + // Reset old value. + mThreadLocal->SetCurrentTransaction(mPreviousTransaction); + } + } +}; + +template <typename T> +void SetResultAndDispatchSuccessEvent( + const NotNull<RefPtr<IDBRequest>>& aRequest, + const SafeRefPtr<IDBTransaction>& aTransaction, T& aPtr, + RefPtr<Event> aEvent = nullptr); + +namespace detail { +void DispatchSuccessEvent(const NotNull<RefPtr<IDBRequest>>& aRequest, + const SafeRefPtr<IDBTransaction>& aTransaction, + const RefPtr<Event>& aEvent); + +template <class T> +std::enable_if_t<std::is_same_v<T, IDBDatabase> || std::is_same_v<T, IDBCursor>, + nsresult> +GetResult(JSContext* aCx, T* aDOMObject, JS::MutableHandle<JS::Value> aResult) { + if (!aDOMObject) { + aResult.setNull(); + return NS_OK; + } + + const 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, const JS::Handle<JS::Value>* aValue, + JS::MutableHandle<JS::Value> aResult) { + aResult.set(*aValue); + return NS_OK; +} + +nsresult GetResult(JSContext* aCx, const uint64_t* aValue, + JS::MutableHandle<JS::Value> aResult) { + aResult.set(JS::NumberValue(*aValue)); + return NS_OK; +} + +nsresult GetResult(JSContext* aCx, StructuredCloneReadInfoChild&& aCloneInfo, + JS::MutableHandle<JS::Value> aResult) { + const bool ok = + IDBObjectStore::DeserializeValue(aCx, std::move(aCloneInfo), aResult); + + if (NS_WARN_IF(!ok)) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + return NS_OK; +} + +nsresult GetResult(JSContext* aCx, StructuredCloneReadInfoChild* aCloneInfo, + JS::MutableHandle<JS::Value> aResult) { + return GetResult(aCx, std::move(*aCloneInfo), aResult); +} + +nsresult GetResult(JSContext* aCx, + nsTArray<StructuredCloneReadInfoChild>* 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 = aCloneInfos->ElementAt(index); + + JS::Rooted<JS::Value> value(aCx); + + const nsresult rv = GetResult(aCx, std::move(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) { + const 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); + + const 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; +} +} // namespace detail + +auto DeserializeStructuredCloneFiles( + IDBDatabase* aDatabase, + const nsTArray<SerializedStructuredCloneFile>& aSerializedFiles, + bool aForPreprocess) { + MOZ_ASSERT_IF(aForPreprocess, aSerializedFiles.Length() == 1); + + return TransformIntoNewArray( + aSerializedFiles, + [aForPreprocess, &database = *aDatabase]( + const auto& serializedFile) -> StructuredCloneFileChild { + MOZ_ASSERT_IF( + aForPreprocess, + serializedFile.type() == StructuredCloneFileBase::eStructuredClone); + + const NullableBlob& blob = serializedFile.file(); + + switch (serializedFile.type()) { + case StructuredCloneFileBase::eBlob: { + MOZ_ASSERT(blob.type() == NullableBlob::TIPCBlob); + + const IPCBlob& ipcBlob = blob.get_IPCBlob(); + + const RefPtr<BlobImpl> blobImpl = + IPCBlobUtils::Deserialize(ipcBlob); + MOZ_ASSERT(blobImpl); + + RefPtr<Blob> blob = + Blob::Create(database.GetOwnerGlobal(), blobImpl); + MOZ_ASSERT(blob); + + return {StructuredCloneFileBase::eBlob, std::move(blob)}; + } + + case StructuredCloneFileBase::eStructuredClone: { + if (aForPreprocess) { + MOZ_ASSERT(blob.type() == NullableBlob::TIPCBlob); + + const IPCBlob& ipcBlob = blob.get_IPCBlob(); + + const RefPtr<BlobImpl> blobImpl = + IPCBlobUtils::Deserialize(ipcBlob); + MOZ_ASSERT(blobImpl); + + RefPtr<Blob> blob = + Blob::Create(database.GetOwnerGlobal(), blobImpl); + MOZ_ASSERT(blob); + + return {StructuredCloneFileBase::eStructuredClone, + std::move(blob)}; + } + MOZ_ASSERT(blob.type() == NullableBlob::Tnull_t); + + return StructuredCloneFileChild{ + StructuredCloneFileBase::eStructuredClone}; + } + + case StructuredCloneFileBase::eMutableFile: + case StructuredCloneFileBase::eWasmBytecode: + case StructuredCloneFileBase::eWasmCompiled: { + MOZ_ASSERT(blob.type() == NullableBlob::Tnull_t); + + return StructuredCloneFileChild{serializedFile.type()}; + + // Don't set mBlob, support for storing WebAssembly.Modules has been + // removed in bug 1469395. Support for de-serialization of + // WebAssembly.Modules has been removed in bug 1561876. Support for + // MutableFile has been removed in bug 1500343. Full removal is + // tracked in bug 1487479. + } + + default: + MOZ_CRASH("Should never get here!"); + } + }); +} + +JSStructuredCloneData PreprocessingNotSupported() { + MOZ_CRASH("Preprocessing not (yet) supported!"); +} + +template <typename PreprocessInfoAccessor> +StructuredCloneReadInfoChild DeserializeStructuredCloneReadInfo( + SerializedStructuredCloneReadInfo&& aSerialized, + IDBDatabase* const aDatabase, + PreprocessInfoAccessor preprocessInfoAccessor) { + // XXX Make this a class invariant of SerializedStructuredCloneReadInfo. + MOZ_ASSERT_IF(aSerialized.hasPreprocessInfo(), + 0 == aSerialized.data().data.Size()); + return {aSerialized.hasPreprocessInfo() ? preprocessInfoAccessor() + : std::move(aSerialized.data().data), + DeserializeStructuredCloneFiles(aDatabase, aSerialized.files(), + /* aForPreprocess */ false), + aDatabase}; +} + +// TODO: Remove duplication between DispatchErrorEvent and DispatchSucessEvent. + +void DispatchErrorEvent( + MovingNotNull<RefPtr<IDBRequest>> aRequest, nsresult aErrorCode, + const SafeRefPtr<IDBTransaction>& aTransaction = nullptr, + RefPtr<Event> aEvent = nullptr) { + const RefPtr<IDBRequest> request = std::move(aRequest); + + request->AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_DOM_INDEXEDDB); + + AUTO_PROFILER_LABEL("IndexedDB:DispatchErrorEvent", DOM); + + request->SetError(aErrorCode); + + if (!aEvent) { + // Make an error event and fire it at the target. + aEvent = CreateGenericEvent(request, nsDependentString(kErrorEventType), + eDoesBubble, eCancelable); + } + MOZ_ASSERT(aEvent); + + // XXX This is redundant if we are called from + // DispatchSuccessEvent. + Maybe<AutoSetCurrentTransaction> asct; + if (aTransaction) { + asct.emplace(SomeRef(*aTransaction)); + } + + if (aTransaction && aTransaction->IsInactive()) { + aTransaction->TransitionToActive(); + } + + if (aTransaction) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "Firing %s event with error 0x%x", "%s (0x%" PRIx32 ")", + aTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kErrorEventType), + static_cast<uint32_t>(aErrorCode)); + } else { + IDB_LOG_MARK_CHILD_REQUEST("Firing %s event with error 0x%x", + "%s (0x%" PRIx32 ")", + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kErrorEventType), + static_cast<uint32_t>(aErrorCode)); + } + + IgnoredErrorResult rv; + const bool doDefault = + request->DispatchEvent(*aEvent, CallerType::System, rv); + if (NS_WARN_IF(rv.Failed())) { + return; + } + + MOZ_ASSERT(!aTransaction || aTransaction->IsActive() || + aTransaction->IsAborted() || + aTransaction->WasExplicitlyCommitted()); + + if (aTransaction && aTransaction->IsActive()) { + aTransaction->TransitionToInactive(); + + // 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 (aErrorCode != NS_ERROR_DOM_INDEXEDDB_ABORT_ERR) { + WidgetEvent* const internalEvent = aEvent->WidgetEventPtr(); + MOZ_ASSERT(internalEvent); + + if (internalEvent->mFlags.mExceptionWasRaised) { + aTransaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } else if (doDefault) { + aTransaction->Abort(request); + } + } + } +} + +template <typename T> +void SetResultAndDispatchSuccessEvent( + const NotNull<RefPtr<IDBRequest>>& aRequest, + const SafeRefPtr<IDBTransaction>& aTransaction, T& aPtr, + RefPtr<Event> aEvent) { + const auto autoTransaction = + AutoSetCurrentTransaction{aTransaction.maybeDeref()}; + + AUTO_PROFILER_LABEL("IndexedDB:SetResultAndDispatchSuccessEvent", DOM); + + aRequest->AssertIsOnOwningThread(); + + if (aTransaction && aTransaction->IsAborted()) { + DispatchErrorEvent(aRequest, aTransaction->AbortCode(), aTransaction); + return; + } + + if (!aEvent) { + aEvent = + CreateGenericEvent(aRequest.get(), nsDependentString(kSuccessEventType), + eDoesNotBubble, eNotCancelable); + } + MOZ_ASSERT(aEvent); + + aRequest->SetResult( + [&aPtr](JSContext* aCx, JS::MutableHandle<JS::Value> aResult) { + MOZ_ASSERT(aCx); + return detail::GetResult(aCx, &aPtr, aResult); + }); + + detail::DispatchSuccessEvent(aRequest, aTransaction, aEvent); +} + +namespace detail { +void DispatchSuccessEvent(const NotNull<RefPtr<IDBRequest>>& aRequest, + const SafeRefPtr<IDBTransaction>& aTransaction, + const RefPtr<Event>& aEvent) { + if (aTransaction && aTransaction->IsInactive()) { + aTransaction->TransitionToActive(); + } + + if (aTransaction) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "Firing %s event", "%s", aTransaction->LoggingSerialNumber(), + aRequest->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kSuccessEventType)); + } else { + IDB_LOG_MARK_CHILD_REQUEST("Firing %s event", "%s", + aRequest->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(aEvent, kSuccessEventType)); + } + + MOZ_ASSERT_IF(aTransaction && !aTransaction->WasExplicitlyCommitted(), + aTransaction->IsActive() && !aTransaction->IsAborted()); + + IgnoredErrorResult rv; + aRequest->DispatchEvent(*aEvent, rv); + if (NS_WARN_IF(rv.Failed())) { + return; + } + + WidgetEvent* const internalEvent = aEvent->WidgetEventPtr(); + MOZ_ASSERT(internalEvent); + + if (aTransaction && aTransaction->IsActive()) { + aTransaction->TransitionToInactive(); + + if (internalEvent->mFlags.mExceptionWasRaised) { + aTransaction->Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } else { + // To handle upgrade transaction. + aTransaction->CommitIfNotStarted(); + } + } +} +} // namespace detail + +PRFileDesc* GetFileDescriptorFromStream(nsIInputStream* aStream) { + MOZ_ASSERT(aStream); + + const nsCOMPtr<nsIFileMetadata> fileMetadata = do_QueryInterface(aStream); + if (NS_WARN_IF(!fileMetadata)) { + return nullptr; + } + + PRFileDesc* fileDesc; + const nsresult rv = fileMetadata->GetFileDescriptor(&fileDesc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + MOZ_ASSERT(fileDesc); + + return fileDesc; +} + +auto GetKeyOperator(const IDBCursorDirection aDirection) { + switch (aDirection) { + case IDBCursorDirection::Next: + case IDBCursorDirection::Nextunique: + return &Key::operator>=; + case IDBCursorDirection::Prev: + case IDBCursorDirection::Prevunique: + return &Key::operator<=; + default: + MOZ_CRASH("Should never get here."); + } +} + +// Does not need to be threadsafe since this only runs on one thread, but +// inheriting from CancelableRunnable is easy. +template <typename T> +class DelayedActionRunnable final : public CancelableRunnable { + using ActionFunc = void (T::*)(); + + SafeRefPtr<T> mActor; + RefPtr<IDBRequest> mRequest; + ActionFunc mActionFunc; + + public: + explicit DelayedActionRunnable(SafeRefPtr<T> aActor, ActionFunc aActionFunc) + : CancelableRunnable("indexedDB::DelayedActionRunnable"), + mActor(std::move(aActor)), + mRequest(mActor->GetRequest()), + mActionFunc(aActionFunc) { + MOZ_ASSERT(mActor); + mActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mActionFunc); + } + + private: + ~DelayedActionRunnable() = default; + + NS_DECL_NSIRUNNABLE + nsresult Cancel() override; +}; + +} // namespace + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +// DiscardableRunnable is used to make workers happy. +class BackgroundRequestChild::PreprocessHelper final + : public DiscardableRunnable, + public nsIInputStreamCallback, + public nsIFileMetadataCallback { + enum class State { + // Just created on the owning thread, dispatched to the thread pool. Next + // step is either Finishing if stream was ready to be read or + // WaitingForStreamReady if the stream is not ready. + Initial, + + // Waiting for stream to be ready on a thread pool thread. Next state is + // Finishing. + WaitingForStreamReady, + + // Waiting to finish/finishing on the owning thread. Next step is Completed. + Finishing, + + // All done. + Completed + }; + + const nsCOMPtr<nsIEventTarget> mOwningEventTarget; + RefPtr<TaskQueue> mTaskQueue; + nsCOMPtr<nsIInputStream> mStream; + UniquePtr<JSStructuredCloneData> mCloneData; + BackgroundRequestChild* mActor; + const uint32_t mCloneDataIndex; + nsresult mResultCode; + State mState; + + public: + PreprocessHelper(uint32_t aCloneDataIndex, BackgroundRequestChild* aActor) + : DiscardableRunnable( + "indexedDB::BackgroundRequestChild::PreprocessHelper"), + mOwningEventTarget(aActor->GetActorEventTarget()), + mActor(aActor), + mCloneDataIndex(aCloneDataIndex), + mResultCode(NS_OK), + mState(State::Initial) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + aActor->AssertIsOnOwningThread(); + } + + bool IsOnOwningThread() const { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && + current; + } + + void AssertIsOnOwningThread() const { MOZ_ASSERT(IsOnOwningThread()); } + + void ClearActor() { + AssertIsOnOwningThread(); + + mActor = nullptr; + } + + nsresult Init(const StructuredCloneFileChild& aFile); + + nsresult Dispatch(); + + private: + ~PreprocessHelper() { + MOZ_ASSERT(mState == State::Initial || mState == State::Completed); + + if (mTaskQueue) { + mTaskQueue->BeginShutdown(); + } + } + + nsresult Start(); + + nsresult ProcessStream(); + + void Finish(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIFILEMETADATACALLBACK +}; + +/******************************************************************************* + * BackgroundRequestChildBase + ******************************************************************************/ + +BackgroundRequestChildBase::BackgroundRequestChildBase( + MovingNotNull<RefPtr<IDBRequest>> aRequest) + : mRequest(std::move(aRequest)) { + mRequest->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundRequestChildBase); +} + +BackgroundRequestChildBase::~BackgroundRequestChildBase() { + AssertIsOnOwningThread(); + + MOZ_COUNT_DTOR(indexedDB::BackgroundRequestChildBase); +} + +#ifdef DEBUG + +void BackgroundRequestChildBase::AssertIsOnOwningThread() const { + mRequest->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +/******************************************************************************* + * BackgroundFactoryChild + ******************************************************************************/ + +BackgroundFactoryChild::BackgroundFactoryChild(IDBFactory& aFactory) + : mFactory(&aFactory) { + AssertIsOnOwningThread(); + mFactory->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundFactoryChild); +} + +BackgroundFactoryChild::~BackgroundFactoryChild() { + MOZ_COUNT_DTOR(indexedDB::BackgroundFactoryChild); +} + +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) const { + AssertIsOnOwningThread(); + + auto* const 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; +} + +mozilla::ipc::IPCResult +BackgroundFactoryChild::RecvPBackgroundIDBDatabaseConstructor( + PBackgroundIDBDatabaseChild* aActor, const DatabaseSpec& aSpec, + NotNull<PBackgroundIDBFactoryRequestChild*> aRequest) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + + return IPC_OK(); +} + +/******************************************************************************* + * BackgroundFactoryRequestChild + ******************************************************************************/ + +BackgroundFactoryRequestChild::BackgroundFactoryRequestChild( + SafeRefPtr<IDBFactory> aFactory, + MovingNotNull<RefPtr<IDBOpenDBRequest>> aOpenRequest, bool aIsDeleteOp, + uint64_t aRequestedVersion) + : BackgroundRequestChildBase(std::move(aOpenRequest)), + mFactory(std::move(aFactory)), + mDatabaseActor(nullptr), + mRequestedVersion(aRequestedVersion), + mIsDeleteOp(aIsDeleteOp) { + // Can't assert owning thread here because IPDL has not yet set our manager! + MOZ_ASSERT(mFactory); + mFactory->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundFactoryRequestChild); +} + +BackgroundFactoryRequestChild::~BackgroundFactoryRequestChild() { + MOZ_COUNT_DTOR(indexedDB::BackgroundFactoryRequestChild); +} + +NotNull<IDBOpenDBRequest*> BackgroundFactoryRequestChild::GetOpenDBRequest() + const { + AssertIsOnOwningThread(); + + // XXX NotNull might provide something to encapsulate this + return WrapNotNullUnchecked( + static_cast<IDBOpenDBRequest*>(mRequest.get().get())); +} + +void BackgroundFactoryRequestChild::SetDatabaseActor( + BackgroundDatabaseChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aActor || !mDatabaseActor); + + mDatabaseActor = aActor; +} + +void 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); + + if (mDatabaseActor) { + mDatabaseActor->ReleaseDOMObject(); + MOZ_ASSERT(!mDatabaseActor); + } +} + +void BackgroundFactoryRequestChild::HandleResponse( + const OpenDatabaseRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + mRequest->Reset(); + + auto* databaseActor = static_cast<BackgroundDatabaseChild*>( + aResponse.database().AsChild().get()); + MOZ_ASSERT(databaseActor); + + IDBDatabase* const database = [this, databaseActor]() -> IDBDatabase* { + IDBDatabase* database = databaseActor->GetDOMObject(); + if (!database) { + Unused << this; + + if (NS_WARN_IF(!databaseActor->EnsureDOMObject())) { + return nullptr; + } + MOZ_ASSERT(mDatabaseActor); + + database = databaseActor->GetDOMObject(); + MOZ_ASSERT(database); + + MOZ_ASSERT(!database->IsClosed()); + } + + return database; + }(); + + if (!database || 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 { + SetResultAndDispatchSuccessEvent(mRequest, nullptr, *database); + } + + if (database) { + MOZ_ASSERT(mDatabaseActor == databaseActor); + + databaseActor->ReleaseDOMObject(); + } else { + databaseActor->SendDeleteMeInternal(); + } + MOZ_ASSERT(!mDatabaseActor); +} + +void BackgroundFactoryRequestChild::HandleResponse( + const DeleteDatabaseRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + RefPtr<Event> successEvent = IDBVersionChangeEvent::Create( + mRequest.get(), nsDependentString(kSuccessEventType), + aResponse.previousVersion()); + MOZ_ASSERT(successEvent); + + SetResultAndDispatchSuccessEvent(mRequest, nullptr, JS::UndefinedHandleValue, + std::move(successEvent)); + + MOZ_ASSERT(!mDatabaseActor); +} + +void BackgroundFactoryRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (aWhy != Deletion) { + GetOpenDBRequest()->NoteComplete(); + } +} + +mozilla::ipc::IPCResult BackgroundFactoryRequestChild::Recv__delete__( + const FactoryRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + switch (aResponse.type()) { + case FactoryRequestResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case FactoryRequestResponse::TOpenDatabaseRequestResponse: + HandleResponse(aResponse.get_OpenDatabaseRequestResponse()); + break; + + case FactoryRequestResponse::TDeleteDatabaseRequestResponse: + HandleResponse(aResponse.get_DeleteDatabaseRequestResponse()); + break; + + default: + return IPC_FAIL(this, "Unknown response type!"); + } + + auto request = GetOpenDBRequest(); + request->NoteComplete(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult BackgroundFactoryRequestChild::RecvBlocked( + const uint64_t aCurrentVersion) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + const nsDependentString type(kBlockedEventType); + + RefPtr<Event> blockedEvent; + if (mIsDeleteOp) { + blockedEvent = + IDBVersionChangeEvent::Create(mRequest.get(), type, aCurrentVersion); + MOZ_ASSERT(blockedEvent); + } else { + blockedEvent = IDBVersionChangeEvent::Create( + mRequest.get(), type, aCurrentVersion, mRequestedVersion); + MOZ_ASSERT(blockedEvent); + } + + RefPtr<IDBRequest> kungFuDeathGrip = mRequest; + + IDB_LOG_MARK_CHILD_REQUEST("Firing \"blocked\" event", "\"blocked\"", + kungFuDeathGrip->LoggingSerialNumber()); + + IgnoredErrorResult rv; + kungFuDeathGrip->DispatchEvent(*blockedEvent, rv); + if (rv.Failed()) { + NS_WARNING("Failed to dispatch event!"); + } + + return IPC_OK(); +} + +/******************************************************************************* + * BackgroundDatabaseChild + ******************************************************************************/ + +BackgroundDatabaseChild::BackgroundDatabaseChild( + const DatabaseSpec& aSpec, BackgroundFactoryRequestChild* aOpenRequestActor) + : mSpec(MakeUnique<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); +} + +#ifdef DEBUG + +void BackgroundDatabaseChild::AssertIsOnOwningThread() const { + static_cast<BackgroundFactoryChild*>(Manager())->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void BackgroundDatabaseChild::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mTemporaryStrongDatabase); + MOZ_ASSERT(!mOpenRequestActor); + + if (mDatabase) { + mDatabase->ClearBackgroundActor(); + mDatabase = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundIDBDatabaseChild::SendDeleteMe()); + } +} + +bool BackgroundDatabaseChild::EnsureDOMObject() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenRequestActor); + + if (mTemporaryStrongDatabase) { + MOZ_ASSERT(!mSpec); + MOZ_ASSERT(mDatabase == mTemporaryStrongDatabase); + return true; + } + + MOZ_ASSERT(mSpec); + + const auto request = mOpenRequestActor->GetOpenDBRequest(); + + auto& factory = + static_cast<BackgroundFactoryChild*>(Manager())->GetDOMObject(); + + if (!factory.GetParentObject()) { + // Already disconnected from global. + + // We need to clear mOpenRequestActor here, since that would otherwise be + // done by ReleaseDOMObject, which cannot be called if EnsureDOMObject + // failed. + mOpenRequestActor = nullptr; + + return false; + } + + // TODO: This AcquireStrongRefFromRawPtr looks suspicious. This should be + // changed or at least well explained, see also comment on + // BackgroundFactoryChild. + mTemporaryStrongDatabase = IDBDatabase::Create( + request, SafeRefPtr{&factory, AcquireStrongRefFromRawPtr{}}, this, + std::move(mSpec)); + + MOZ_ASSERT(mTemporaryStrongDatabase); + mTemporaryStrongDatabase->AssertIsOnOwningThread(); + + mDatabase = mTemporaryStrongDatabase; + + mOpenRequestActor->SetDatabaseActor(this); + + return true; +} + +void BackgroundDatabaseChild::ReleaseDOMObject() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTemporaryStrongDatabase); + mTemporaryStrongDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(mOpenRequestActor); + MOZ_ASSERT(mDatabase == mTemporaryStrongDatabase); + + mOpenRequestActor->SetDatabaseActor(nullptr); + + 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; + + // XXX Why isn't mDatabase set to nullptr here? +} + +void BackgroundDatabaseChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mDatabase) { + mDatabase->ClearBackgroundActor(); +#ifdef DEBUG + mDatabase = nullptr; +#endif + } +} + +PBackgroundIDBDatabaseFileChild* +BackgroundDatabaseChild::AllocPBackgroundIDBDatabaseFileChild( + const IPCBlob& aIPCBlob) { + MOZ_CRASH("PBackgroundIDBFileChild actors should be manually constructed!"); +} + +bool BackgroundDatabaseChild::DeallocPBackgroundIDBDatabaseFileChild( + PBackgroundIDBDatabaseFileChild* aActor) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + + delete aActor; + return true; +} + +already_AddRefed<PBackgroundIDBVersionChangeTransactionChild> +BackgroundDatabaseChild::AllocPBackgroundIDBVersionChangeTransactionChild( + const uint64_t aCurrentVersion, const uint64_t aRequestedVersion, + const int64_t aNextObjectStoreId, const int64_t aNextIndexId) { + AssertIsOnOwningThread(); + + return RefPtr{new BackgroundVersionChangeTransactionChild( + mOpenRequestActor->GetOpenDBRequest())} + .forget(); +} + +mozilla::ipc::IPCResult +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(); + + auto* const actor = + static_cast<BackgroundVersionChangeTransactionChild*>(aActor); + + if (!EnsureDOMObject()) { + NS_WARNING("Factory is already disconnected from global"); + + actor->SendDeleteMeInternal(true); + + // XXX This is a hack to ensure that transaction/request serial numbers stay + // in sync between parent and child. Actually, it might be better to create + // an IDBTransaction in the child and abort that. + Unused + << mozilla::ipc::BackgroundChildImpl::GetThreadLocalForCurrentThread() + ->mIndexedDBThreadLocal->NextTransactionSN( + IDBTransaction::Mode::VersionChange); + Unused << IDBRequest::NextSerialNumber(); + + // No reason to IPC_FAIL here. + return IPC_OK(); + } + + MOZ_ASSERT(!mDatabase->IsInvalidated()); + + // XXX NotNull might encapsulate this + const auto request = + WrapNotNullUnchecked(RefPtr{mOpenRequestActor->GetOpenDBRequest().get()}); + + SafeRefPtr<IDBTransaction> transaction = IDBTransaction::CreateVersionChange( + mDatabase, actor, request, aNextObjectStoreId, aNextIndexId); + MOZ_ASSERT(transaction); + + transaction->AssertIsOnOwningThread(); + + actor->SetDOMTransaction(transaction.clonePtr()); + + const auto database = WrapNotNull(mDatabase); + + database->EnterSetVersionTransaction(aRequestedVersion); + + request->SetTransaction(transaction.clonePtr()); + + RefPtr<Event> upgradeNeededEvent = IDBVersionChangeEvent::Create( + request.get(), nsDependentString(kUpgradeNeededEventType), + aCurrentVersion, aRequestedVersion); + MOZ_ASSERT(upgradeNeededEvent); + + SetResultAndDispatchSuccessEvent( + WrapNotNullUnchecked<RefPtr<IDBRequest>>(request.get()), transaction, + *database, std::move(upgradeNeededEvent)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult BackgroundDatabaseChild::RecvVersionChange( + const uint64_t aOldVersion, const Maybe<uint64_t> aNewVersion) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (!mDatabase || mDatabase->IsClosed()) { + return IPC_OK(); + } + + 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 (owner->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 IPC_OK(); + } + } + + // Otherwise fire a versionchange event. + const nsDependentString type(kVersionChangeEventType); + + RefPtr<Event> versionChangeEvent; + + if (aNewVersion.isNothing()) { + versionChangeEvent = + IDBVersionChangeEvent::Create(kungFuDeathGrip, type, aOldVersion); + MOZ_ASSERT(versionChangeEvent); + } else { + versionChangeEvent = IDBVersionChangeEvent::Create( + kungFuDeathGrip, type, aOldVersion, aNewVersion.value()); + MOZ_ASSERT(versionChangeEvent); + } + + IDB_LOG_MARK("Child : Firing \"versionchange\" event", + "C: IDBDatabase \"versionchange\" event", IDB_LOG_ID_STRING()); + + IgnoredErrorResult rv; + kungFuDeathGrip->DispatchEvent(*versionChangeEvent, rv); + if (rv.Failed()) { + NS_WARNING("Failed to dispatch event!"); + } + + if (!kungFuDeathGrip->IsClosed()) { + SendBlocked(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult BackgroundDatabaseChild::RecvInvalidate() { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mDatabase) { + mDatabase->Invalidate(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +BackgroundDatabaseChild::RecvCloseAfterInvalidationComplete() { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (mDatabase) { + mDatabase->DispatchTrustedEvent(nsDependentString(kCloseEventType)); + } + + return IPC_OK(); +} + +/******************************************************************************* + * BackgroundTransactionBase + ******************************************************************************/ + +BackgroundTransactionBase::BackgroundTransactionBase( + SafeRefPtr<IDBTransaction> aTransaction) + : mTemporaryStrongTransaction(std::move(aTransaction)), + mTransaction(mTemporaryStrongTransaction.unsafeGetRawPtr()) { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(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( + SafeRefPtr<IDBTransaction> aTransaction) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + MOZ_ASSERT(!mTemporaryStrongTransaction); + MOZ_ASSERT(!mTransaction); + + mTemporaryStrongTransaction = std::move(aTransaction); + mTransaction = mTemporaryStrongTransaction.unsafeGetRawPtr(); +} + +void BackgroundTransactionBase::NoteComplete() { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mTransaction, mTemporaryStrongTransaction); + + mTemporaryStrongTransaction = nullptr; +} + +/******************************************************************************* + * BackgroundTransactionChild + ******************************************************************************/ + +BackgroundTransactionChild::BackgroundTransactionChild( + SafeRefPtr<IDBTransaction> aTransaction) + : BackgroundTransactionBase(std::move(aTransaction)) { + 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(); +} + +mozilla::ipc::IPCResult BackgroundTransactionChild::RecvComplete( + const nsresult aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + MaybeCollectGarbageOnIPCMessage(); + + mTransaction->FireCompleteOrAbortEvents(aResult); + + NoteComplete(); + return IPC_OK(); +} + +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 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(); +} + +mozilla::ipc::IPCResult BackgroundVersionChangeTransactionChild::RecvComplete( + const nsresult aResult) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + if (!mTransaction) { + return IPC_OK(); + } + + MOZ_ASSERT(mOpenDBRequest); + + IDBDatabase* database = mTransaction->Database(); + MOZ_ASSERT(database); + + database->ExitSetVersionTransaction(); + + if (NS_FAILED(aResult)) { + database->Close(); + } + + RefPtr<IDBOpenDBRequest> request = mOpenDBRequest; + MOZ_ASSERT(request); + + mTransaction->FireCompleteOrAbortEvents(aResult); + + request->SetTransaction(nullptr); + request = nullptr; + + mOpenDBRequest = nullptr; + + NoteComplete(); + return IPC_OK(); +} + +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 aActor; + return true; +} + +/******************************************************************************* + * BackgroundRequestChild + ******************************************************************************/ + +BackgroundRequestChild::BackgroundRequestChild( + MovingNotNull<RefPtr<IDBRequest>> aRequest) + : BackgroundRequestChildBase(std::move(aRequest)), + mTransaction(mRequest->AcquireTransaction()), + mRunningPreprocessHelpers(0), + mCurrentCloneDataIndex(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 aCloneDataIndex, UniquePtr<JSStructuredCloneData> aCloneData) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCloneDataIndex < mCloneInfos.Length()); + MOZ_ASSERT(aCloneData); + + auto& cloneInfo = mCloneInfos[aCloneDataIndex]; + MOZ_ASSERT(cloneInfo.mPreprocessHelper); + MOZ_ASSERT(!cloneInfo.mCloneData); + + cloneInfo.mCloneData = std::move(aCloneData); + + MaybeSendContinue(); + + cloneInfo.mPreprocessHelper = nullptr; +} + +void BackgroundRequestChild::OnPreprocessFailed(uint32_t aCloneDataIndex, + nsresult aErrorCode) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCloneDataIndex < mCloneInfos.Length()); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + + auto& cloneInfo = mCloneInfos[aCloneDataIndex]; + MOZ_ASSERT(cloneInfo.mPreprocessHelper); + MOZ_ASSERT(!cloneInfo.mCloneData); + + if (NS_SUCCEEDED(mPreprocessResultCode)) { + mPreprocessResultCode = aErrorCode; + } + + MaybeSendContinue(); + + cloneInfo.mPreprocessHelper = nullptr; +} + +UniquePtr<JSStructuredCloneData> BackgroundRequestChild::GetNextCloneData() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mCurrentCloneDataIndex < mCloneInfos.Length()); + MOZ_ASSERT(mCloneInfos[mCurrentCloneDataIndex].mCloneData); + + return std::move(mCloneInfos[mCurrentCloneDataIndex++].mCloneData); +} + +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.clonePtr()); +} + +void BackgroundRequestChild::HandleResponse(const Key& aResponse) { + AssertIsOnOwningThread(); + + SetResultAndDispatchSuccessEvent(mRequest, AcquireTransaction(), aResponse); +} + +void BackgroundRequestChild::HandleResponse(const nsTArray<Key>& aResponse) { + AssertIsOnOwningThread(); + + SetResultAndDispatchSuccessEvent(mRequest, AcquireTransaction(), aResponse); +} + +void BackgroundRequestChild::HandleResponse( + SerializedStructuredCloneReadInfo&& aResponse) { + AssertIsOnOwningThread(); + + auto cloneReadInfo = DeserializeStructuredCloneReadInfo( + std::move(aResponse), mTransaction->Database(), + [this] { return std::move(*GetNextCloneData()); }); + + SetResultAndDispatchSuccessEvent(mRequest, AcquireTransaction(), + cloneReadInfo); +} + +void BackgroundRequestChild::HandleResponse( + nsTArray<SerializedStructuredCloneReadInfo>&& aResponse) { + AssertIsOnOwningThread(); + + nsTArray<StructuredCloneReadInfoChild> cloneReadInfos; + + QM_TRY(OkIf(cloneReadInfos.SetCapacity(aResponse.Length(), fallible)), + QM_VOID, ([&aResponse, this](const auto) { + // Since we are under memory pressure, release aResponse early. + aResponse.Clear(); + + DispatchErrorEvent(mRequest, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + AcquireTransaction()); + + MOZ_ASSERT(mTransaction->IsAborted()); + })); + + std::transform(std::make_move_iterator(aResponse.begin()), + std::make_move_iterator(aResponse.end()), + MakeBackInserter(cloneReadInfos), + [database = mTransaction->Database(), this]( + SerializedStructuredCloneReadInfo&& serializedCloneInfo) { + return DeserializeStructuredCloneReadInfo( + std::move(serializedCloneInfo), database, + [this] { return std::move(*GetNextCloneData()); }); + }); + + SetResultAndDispatchSuccessEvent(mRequest, AcquireTransaction(), + cloneReadInfos); +} + +void BackgroundRequestChild::HandleResponse(JS::Handle<JS::Value> aResponse) { + AssertIsOnOwningThread(); + + SetResultAndDispatchSuccessEvent( + mRequest, AcquireTransaction(), + const_cast<const JS::Handle<JS::Value>&>(aResponse)); +} + +void BackgroundRequestChild::HandleResponse(const uint64_t aResponse) { + AssertIsOnOwningThread(); + + SetResultAndDispatchSuccessEvent(mRequest, AcquireTransaction(), aResponse); +} + +nsresult BackgroundRequestChild::HandlePreprocess( + const PreprocessInfo& aPreprocessInfo) { + return HandlePreprocessInternal( + AutoTArray<PreprocessInfo, 1>{aPreprocessInfo}); +} + +nsresult BackgroundRequestChild::HandlePreprocess( + const nsTArray<PreprocessInfo>& aPreprocessInfos) { + AssertIsOnOwningThread(); + mGetAll = true; + + return HandlePreprocessInternal(aPreprocessInfos); +} + +nsresult BackgroundRequestChild::HandlePreprocessInternal( + const nsTArray<PreprocessInfo>& aPreprocessInfos) { + AssertIsOnOwningThread(); + + IDBDatabase* database = mTransaction->Database(); + + const uint32_t count = aPreprocessInfos.Length(); + + mCloneInfos.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 PreprocessInfo& preprocessInfo = aPreprocessInfos[index]; + + const auto files = + DeserializeStructuredCloneFiles(database, preprocessInfo.files(), + /* aForPreprocess */ true); + + MOZ_ASSERT(files.Length() == 1); + + auto& preprocessHelper = mCloneInfos[index].mPreprocessHelper; + preprocessHelper = MakeRefPtr<PreprocessHelper>(index, this); + + nsresult rv = preprocessHelper->Init(files[0]); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = preprocessHelper->Dispatch(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mRunningPreprocessHelpers++; + } + + return NS_OK; +} + +void BackgroundRequestChild::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + MaybeCollectGarbageOnIPCMessage(); + + for (auto& cloneInfo : mCloneInfos) { + const auto& preprocessHelper = cloneInfo.mPreprocessHelper; + + if (preprocessHelper) { + preprocessHelper->ClearActor(); + } + } + mCloneInfos.Clear(); + + if (mTransaction) { + mTransaction->AssertIsOnOwningThread(); + + mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ + aWhy == Deletion); +#ifdef DEBUG + mTransaction = nullptr; +#endif + } +} + +mozilla::ipc::IPCResult BackgroundRequestChild::Recv__delete__( + RequestResponse&& aResponse) { + AssertIsOnOwningThread(); + 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( + std::move(aResponse.get_ObjectStoreGetResponse().cloneInfo())); + break; + + case RequestResponse::TObjectStoreGetKeyResponse: + HandleResponse(aResponse.get_ObjectStoreGetKeyResponse().key()); + break; + + case RequestResponse::TObjectStoreGetAllResponse: + HandleResponse( + std::move(aResponse.get_ObjectStoreGetAllResponse().cloneInfos())); + break; + + case RequestResponse::TObjectStoreGetAllKeysResponse: + HandleResponse(aResponse.get_ObjectStoreGetAllKeysResponse().keys()); + break; + + case RequestResponse::TObjectStoreDeleteResponse: + case RequestResponse::TObjectStoreClearResponse: + HandleResponse(JS::UndefinedHandleValue); + break; + + case RequestResponse::TObjectStoreCountResponse: + HandleResponse(aResponse.get_ObjectStoreCountResponse().count()); + break; + + case RequestResponse::TIndexGetResponse: + HandleResponse(std::move(aResponse.get_IndexGetResponse().cloneInfo())); + break; + + case RequestResponse::TIndexGetKeyResponse: + HandleResponse(aResponse.get_IndexGetKeyResponse().key()); + break; + + case RequestResponse::TIndexGetAllResponse: + HandleResponse( + std::move(aResponse.get_IndexGetAllResponse().cloneInfos())); + break; + + case RequestResponse::TIndexGetAllKeysResponse: + HandleResponse(aResponse.get_IndexGetAllKeysResponse().keys()); + break; + + case RequestResponse::TIndexCountResponse: + HandleResponse(aResponse.get_IndexCountResponse().count()); + break; + + default: + return IPC_FAIL(this, "Unknown response type!"); + } + } + + mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ true); + + // Null this out so that we don't try to call OnRequestFinished() again in + // ActorDestroy. + mTransaction = nullptr; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult BackgroundRequestChild::RecvPreprocess( + const PreprocessParams& aParams) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + MaybeCollectGarbageOnIPCMessage(); + + nsresult rv; + + switch (aParams.type()) { + case PreprocessParams::TObjectStoreGetPreprocessParams: { + const auto& params = aParams.get_ObjectStoreGetPreprocessParams(); + + rv = HandlePreprocess(params.preprocessInfo()); + + break; + } + + case PreprocessParams::TObjectStoreGetAllPreprocessParams: { + const auto& params = aParams.get_ObjectStoreGetAllPreprocessParams(); + + rv = HandlePreprocess(params.preprocessInfos()); + + break; + } + + default: + return IPC_FAIL(this, "Unknown params type!"); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + QM_WARNONLY_TRY(OkIf(SendContinue(rv))); + } + + return IPC_OK(); +} + +nsresult BackgroundRequestChild::PreprocessHelper::Init( + const StructuredCloneFileChild& aFile) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aFile.HasBlob()); + MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eStructuredClone); + MOZ_ASSERT(mState == State::Initial); + + // 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); + + // We use a TaskQueue here in order to be sure that the events are dispatched + // in the correct order. This is not guaranteed in case we use the I/O thread + // directly. + mTaskQueue = TaskQueue::Create(target.forget(), "BackgroundRequestChild"); + + ErrorResult errorResult; + + nsCOMPtr<nsIInputStream> stream; + // XXX After Bug 1620560, MutableBlob is not needed here anymore. + aFile.MutableBlob().CreateInputStream(getter_AddRefs(stream), errorResult); + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + mStream = std::move(stream); + + mCloneData = MakeUnique<JSStructuredCloneData>( + JS::StructuredCloneScope::DifferentProcessForIndexedDB); + + return NS_OK; +} + +nsresult BackgroundRequestChild::PreprocessHelper::Dispatch() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Initial); + + nsresult rv = mTaskQueue->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult BackgroundRequestChild::PreprocessHelper::Start() { + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(mStream); + MOZ_ASSERT(mState == State::Initial); + + nsresult rv; + + PRFileDesc* fileDesc = GetFileDescriptorFromStream(mStream); + if (fileDesc) { + rv = ProcessStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + mState = State::WaitingForStreamReady; + + nsCOMPtr<nsIAsyncFileMetadata> asyncFileMetadata = do_QueryInterface(mStream); + if (asyncFileMetadata) { + rv = asyncFileMetadata->AsyncFileMetadataWait(this, mTaskQueue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(mStream); + if (!asyncStream) { + return NS_ERROR_NO_INTERFACE; + } + + rv = asyncStream->AsyncWait(this, 0, 0, mTaskQueue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult BackgroundRequestChild::PreprocessHelper::ProcessStream() { + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(mStream); + MOZ_ASSERT(mState == State::Initial || + mState == State::WaitingForStreamReady); + + // We need to get the internal stream (which is an nsFileInputStream) because + // SnappyUncompressInputStream doesn't support reading from async input + // streams. + + nsCOMPtr<mozIRemoteLazyInputStream> blobInputStream = + do_QueryInterface(mStream); + MOZ_ASSERT(blobInputStream); + + nsCOMPtr<nsIInputStream> internalInputStream; + MOZ_ALWAYS_SUCCEEDS( + blobInputStream->TakeInternalStream(getter_AddRefs(internalInputStream))); + MOZ_ASSERT(internalInputStream); + + QM_TRY(MOZ_TO_RESULT( + SnappyUncompressStructuredCloneData(*internalInputStream, *mCloneData))); + + mState = State::Finishing; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +void BackgroundRequestChild::PreprocessHelper::Finish() { + AssertIsOnOwningThread(); + + if (mActor) { + if (NS_SUCCEEDED(mResultCode)) { + mActor->OnPreprocessFinished(mCloneDataIndex, std::move(mCloneData)); + + MOZ_ASSERT(!mCloneData); + } else { + mActor->OnPreprocessFailed(mCloneDataIndex, mResultCode); + } + } + + mState = State::Completed; +} + +NS_IMPL_ISUPPORTS_INHERITED(BackgroundRequestChild::PreprocessHelper, + DiscardableRunnable, nsIInputStreamCallback, + nsIFileMetadataCallback) + +NS_IMETHODIMP +BackgroundRequestChild::PreprocessHelper::Run() { + nsresult rv; + + switch (mState) { + case State::Initial: + rv = Start(); + break; + + case State::WaitingForStreamReady: + rv = ProcessStream(); + 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 (IsOnOwningThread()) { + Finish(); + } else { + MOZ_ALWAYS_SUCCEEDS( + mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +BackgroundRequestChild::PreprocessHelper::OnInputStreamReady( + nsIAsyncInputStream* aStream) { + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(mState == State::WaitingForStreamReady); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); + + return NS_OK; +} + +NS_IMETHODIMP +BackgroundRequestChild::PreprocessHelper::OnFileMetadataReady( + nsIAsyncFileMetadata* aObject) { + MOZ_ASSERT(!IsOnOwningThread()); + MOZ_ASSERT(mState == State::WaitingForStreamReady); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); + + return NS_OK; +} + +/******************************************************************************* + * BackgroundCursorChild + ******************************************************************************/ + +BackgroundCursorChildBase::BackgroundCursorChildBase( + const NotNull<IDBRequest*> aRequest, const Direction aDirection) + : mRequest(aRequest), + mTransaction(aRequest->MaybeTransactionRef()), + mStrongRequest(aRequest), + mDirection(aDirection) { + MOZ_ASSERT(mTransaction); +} + +MovingNotNull<RefPtr<IDBRequest>> BackgroundCursorChildBase::AcquireRequest() + const { + AssertIsOnOwningThread(); + + // XXX This could be encapsulated by NotNull + return WrapNotNullUnchecked(RefPtr{mRequest->get()}); +} + +template <IDBCursorType CursorType> +BackgroundCursorChild<CursorType>::BackgroundCursorChild( + const NotNull<IDBRequest*> aRequest, SourceType* aSource, + Direction aDirection) + : BackgroundCursorChildBase(aRequest, aDirection), + mSource(WrapNotNull(aSource)), + mCursor(nullptr), + mInFlightResponseInvalidationNeeded(false) { + aSource->AssertIsOnOwningThread(); + + MOZ_COUNT_CTOR(indexedDB::BackgroundCursorChild<CursorType>); +} + +template <IDBCursorType CursorType> +BackgroundCursorChild<CursorType>::~BackgroundCursorChild() { + MOZ_COUNT_DTOR(indexedDB::BackgroundCursorChild<CursorType>); +} + +template <IDBCursorType CursorType> +SafeRefPtr<BackgroundCursorChild<CursorType>> +BackgroundCursorChild<CursorType>::SafeRefPtrFromThis() { + return BackgroundCursorChildBase::SafeRefPtrFromThis() + .template downcast<BackgroundCursorChild>(); +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::SendContinueInternal( + const CursorRequestParams& aParams, + const CursorData<CursorType>& aCurrentData) { + 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(GetRequest()->ReadyState() == IDBRequestReadyState::Done); + GetRequest()->Reset(); + + mTransaction->OnNewRequest(); + + CursorRequestParams params = aParams; + Key currentKey = aCurrentData.mKey; + Key currentObjectStoreKey; + // TODO: This is still not nice. + if constexpr (!CursorTypeTraits<CursorType>::IsObjectStoreCursor) { + currentObjectStoreKey = aCurrentData.mObjectStoreKey; + } + + switch (params.type()) { + case CursorRequestParams::TContinueParams: { + const auto& key = params.get_ContinueParams().key(); + if (key.IsUnset()) { + break; + } + + // Discard cache entries before the target key. + DiscardCachedResponses( + [&key, isLocaleAware = mCursor->IsLocaleAware(), + keyOperator = GetKeyOperator(mDirection), + transactionSerialNumber = mTransaction->LoggingSerialNumber(), + requestSerialNumber = GetRequest()->LoggingSerialNumber()]( + const auto& currentCachedResponse) { + // This duplicates the logic from the parent. We could avoid this + // duplication if we invalidated the cached records always for any + // continue-with-key operation, but would lose the benefits of + // preloading then. + const auto& cachedSortKey = + currentCachedResponse.GetSortKey(isLocaleAware); + const bool discard = !(cachedSortKey.*keyOperator)(key); + if (discard) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Continue to key %s, discarding cached key %s/%s", + "Continue, discarding%.0s%.0s%.0s", transactionSerialNumber, + requestSerialNumber, key.GetBuffer().get(), + cachedSortKey.GetBuffer().get(), + currentCachedResponse.GetObjectStoreKeyForLogging()); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Continue to key %s, keeping cached key %s/%s and " + "further", + "Continue, keeping%.0s%.0s%.0s", transactionSerialNumber, + requestSerialNumber, key.GetBuffer().get(), + cachedSortKey.GetBuffer().get(), + currentCachedResponse.GetObjectStoreKeyForLogging()); + } + + return discard; + }); + + break; + } + + case CursorRequestParams::TContinuePrimaryKeyParams: { + if constexpr (!CursorTypeTraits<CursorType>::IsObjectStoreCursor) { + const auto& key = params.get_ContinuePrimaryKeyParams().key(); + const auto& primaryKey = + params.get_ContinuePrimaryKeyParams().primaryKey(); + if (key.IsUnset() || primaryKey.IsUnset()) { + break; + } + + // Discard cache entries before the target key. + DiscardCachedResponses([&key, &primaryKey, + isLocaleAware = mCursor->IsLocaleAware(), + keyCompareOperator = GetKeyOperator(mDirection), + transactionSerialNumber = + mTransaction->LoggingSerialNumber(), + requestSerialNumber = + GetRequest()->LoggingSerialNumber()]( + const auto& currentCachedResponse) { + // This duplicates the logic from the parent. We could avoid this + // duplication if we invalidated the cached records always for any + // continue-with-key operation, but would lose the benefits of + // preloading then. + const auto& cachedSortKey = + currentCachedResponse.GetSortKey(isLocaleAware); + const auto& cachedSortPrimaryKey = + currentCachedResponse.mObjectStoreKey; + + const bool discard = + (cachedSortKey == key && + !(cachedSortPrimaryKey.*keyCompareOperator)(primaryKey)) || + !(cachedSortKey.*keyCompareOperator)(key); + + if (discard) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Continue to key %s with primary key %s, discarding " + "cached key %s with cached primary key %s", + "Continue, discarding%.0s%.0s%.0s%.0s", transactionSerialNumber, + requestSerialNumber, key.GetBuffer().get(), + primaryKey.GetBuffer().get(), cachedSortKey.GetBuffer().get(), + cachedSortPrimaryKey.GetBuffer().get()); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Continue to key %s with primary key %s, keeping " + "cached key %s with cached primary key %s and further", + "Continue, keeping%.0s%.0s%.0s%.0s", transactionSerialNumber, + requestSerialNumber, key.GetBuffer().get(), + primaryKey.GetBuffer().get(), cachedSortKey.GetBuffer().get(), + cachedSortPrimaryKey.GetBuffer().get()); + } + + return discard; + }); + } else { + MOZ_CRASH("Shouldn't get here"); + } + + break; + } + + case CursorRequestParams::TAdvanceParams: { + uint32_t& advanceCount = params.get_AdvanceParams().count(); + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Advancing %" PRIu32 " records", "Advancing %" PRIu32, + mTransaction->LoggingSerialNumber(), + GetRequest()->LoggingSerialNumber(), advanceCount); + + // Discard cache entries. + DiscardCachedResponses([&advanceCount, ¤tKey, + ¤tObjectStoreKey]( + const auto& currentCachedResponse) { + const bool res = advanceCount > 1; + if (res) { + --advanceCount; + + // TODO: We only need to update currentKey on the last entry, the + // others are overwritten in the next iteration anyway. + currentKey = currentCachedResponse.mKey; + if constexpr (!CursorTypeTraits<CursorType>::IsObjectStoreCursor) { + currentObjectStoreKey = currentCachedResponse.mObjectStoreKey; + } else { + Unused << currentObjectStoreKey; + } + } + return res; + }); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + if (!mCachedResponses.empty()) { + // We need to remove the response here from mCachedResponses, since when + // requests are interleaved, other events may be processed before + // CompleteContinueRequestFromCache, which may modify mCachedResponses. + mDelayedResponses.emplace_back(std::move(mCachedResponses.front())); + mCachedResponses.pop_front(); + + // We cannot send the response right away, as we must preserve the request + // order. Dispatching a DelayedActionRunnable only partially addresses this. + // This is accompanied by invalidating cached entries at proper locations to + // make it correct. To avoid this, further changes are necessary, see Bug + // 1580499. + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread( + MakeAndAddRef<DelayedActionRunnable<BackgroundCursorChild<CursorType>>>( + SafeRefPtrFromThis(), + &BackgroundCursorChild::CompleteContinueRequestFromCache))); + + // TODO: Could we preload further entries in the background when the size of + // mCachedResponses falls under some threshold? Or does the response + // handling model disallow this? + } else { + MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendContinue( + params, currentKey, currentObjectStoreKey)); + } +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::CompleteContinueRequestFromCache() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mStrongCursor); + MOZ_ASSERT(!mDelayedResponses.empty()); + MOZ_ASSERT(mCursor->GetType() == CursorType); + + const RefPtr<IDBCursor> cursor = std::move(mStrongCursor); + + mCursor->Reset(std::move(mDelayedResponses.front())); + mDelayedResponses.pop_front(); + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Consumed 1 cached response, %zu cached responses remaining", + "Consumed cached response, %zu remaining", + mTransaction->LoggingSerialNumber(), GetRequest()->LoggingSerialNumber(), + mDelayedResponses.size() + mCachedResponses.size()); + + SetResultAndDispatchSuccessEvent( + GetRequest(), + mTransaction + ? SafeRefPtr{&mTransaction.ref(), AcquireStrongRefFromRawPtr{}} + : nullptr, + *cursor); + + mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ true); +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::SendDeleteMeInternal() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + mRequest.destroy(); + mTransaction = Nothing(); + // TODO: The things until here could be pulled up to + // BackgroundCursorChildBase. + + mSource.destroy(); + + if (mCursor) { + mCursor->ClearBackgroundActor(); + mCursor = nullptr; + + MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendDeleteMe()); + } +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::InvalidateCachedResponses() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mRequest); + + // TODO: With more information on the reason for the invalidation, we might + // only selectively invalidate cached responses. If the reason is an updated + // value, we do not need to care for key-only cursors. If the key of the + // changed entry is not in the remaining range of the cursor, we also do not + // need to care, etc. + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Invalidating all %zu cached responses", "Invalidating %zu", + mTransaction->LoggingSerialNumber(), GetRequest()->LoggingSerialNumber(), + mCachedResponses.size()); + + mCachedResponses.clear(); + + // We only hold a strong cursor reference in mStrongCursor when + // continue()/similar has been called. In those cases we expect a response + // that will be received in the future, and it may include prefetched data + // that needs to be discarded. + if (mStrongCursor) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Setting flag to invalidate in-flight responses", + "Set flag to invalidate in-flight responses", + mTransaction->LoggingSerialNumber(), + GetRequest()->LoggingSerialNumber()); + + mInFlightResponseInvalidationNeeded = true; + } +} + +template <IDBCursorType CursorType> +template <typename Condition> +void BackgroundCursorChild<CursorType>::DiscardCachedResponses( + const Condition& aConditionFunc) { + size_t discardedCount = 0; + while (!mCachedResponses.empty() && + aConditionFunc(mCachedResponses.front())) { + mCachedResponses.pop_front(); + ++discardedCount; + } + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Discarded %zu cached responses, %zu remaining", + "Discarded %zu; remaining %zu", mTransaction->LoggingSerialNumber(), + GetRequest()->LoggingSerialNumber(), discardedCount, + mCachedResponses.size()); +} + +BackgroundCursorChildBase::~BackgroundCursorChildBase() = default; + +void BackgroundCursorChildBase::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( + GetRequest(), aResponse, + SafeRefPtr{&mTransaction.ref(), AcquireStrongRefFromRawPtr{}}); +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::HandleResponse( + const void_t& aResponse) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + + if (mCursor) { + mCursor->Reset(); + } + + SetResultAndDispatchSuccessEvent( + GetRequest(), + mTransaction + ? SafeRefPtr{&mTransaction.ref(), AcquireStrongRefFromRawPtr{}} + : nullptr, + JS::NullHandleValue); + + if (!mCursor) { + MOZ_ALWAYS_SUCCEEDS(this->GetActorEventTarget()->Dispatch( + MakeAndAddRef<DelayedActionRunnable<BackgroundCursorChild<CursorType>>>( + SafeRefPtrFromThis(), &BackgroundCursorChild::SendDeleteMeInternal), + NS_DISPATCH_NORMAL)); + } +} + +template <IDBCursorType CursorType> +template <typename... Args> +RefPtr<IDBCursor> +BackgroundCursorChild<CursorType>::HandleIndividualCursorResponse( + const bool aUseAsCurrentResult, Args&&... aArgs) { + if (mCursor) { + if (aUseAsCurrentResult) { + mCursor->Reset(CursorData<CursorType>{std::forward<Args>(aArgs)...}); + } else { + mCachedResponses.emplace_back(std::forward<Args>(aArgs)...); + } + return nullptr; + } + + MOZ_ASSERT(aUseAsCurrentResult); + + // TODO: This still looks quite dangerous to me. Why is mCursor not a + // RefPtr? + auto newCursor = IDBCursor::Create(this, std::forward<Args>(aArgs)...); + mCursor = newCursor; + return newCursor; +} + +template <IDBCursorType CursorType> +template <typename Func> +void BackgroundCursorChild<CursorType>::HandleMultipleCursorResponses( + nsTArray<ResponseType>&& aResponses, const Func& aHandleRecord) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(!mStrongRequest); + MOZ_ASSERT(!mStrongCursor); + MOZ_ASSERT(aResponses.Length() > 0); + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Received %zu cursor responses", "Received %zu", + mTransaction->LoggingSerialNumber(), GetRequest()->LoggingSerialNumber(), + aResponses.Length()); + MOZ_ASSERT_IF(aResponses.Length() > 1, mCachedResponses.empty()); + + // If a new cursor is created, we need to keep a reference to it until the + // SetResultAndDispatchSuccessEvent creates a DOM Binding. + RefPtr<IDBCursor> strongNewCursor; + + bool isFirst = true; + for (auto& response : aResponses) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Processing response for key %s", "Processing%.0s", + mTransaction->LoggingSerialNumber(), + GetRequest()->LoggingSerialNumber(), response.key().GetBuffer().get()); + + // TODO: At the moment, we only send a cursor request to the parent if + // requested by the user code. Therefore, the first result is always used + // as the current result, and the potential extra results are cached. If + // we extended this towards preloading in the background, all results + // might need to be cached. + auto maybeNewCursor = + aHandleRecord(/* aUseAsCurrentResult */ isFirst, std::move(response)); + if (maybeNewCursor) { + MOZ_ASSERT(!strongNewCursor); + strongNewCursor = std::move(maybeNewCursor); + } + isFirst = false; + + if (mInFlightResponseInvalidationNeeded) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "PRELOAD: Discarding remaining responses since " + "mInFlightResponseInvalidationNeeded is set", + "Discarding responses", mTransaction->LoggingSerialNumber(), + GetRequest()->LoggingSerialNumber()); + + mInFlightResponseInvalidationNeeded = false; + break; + } + } + + SetResultAndDispatchSuccessEvent( + GetRequest(), + mTransaction + ? SafeRefPtr{&mTransaction.ref(), AcquireStrongRefFromRawPtr{}} + : nullptr, + *static_cast<IDBCursor*>(mCursor)); +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::HandleResponse( + nsTArray<ResponseType>&& aResponses) { + AssertIsOnOwningThread(); + + if constexpr (CursorType == IDBCursorType::ObjectStore || + CursorType == IDBCursorType::Index) { + MOZ_ASSERT(mTransaction); + + if (!mTransaction->Database()->GetOwnerGlobal()) { + // Ignore the response, since we have already been disconnected from the + // global. + return; + } + } + + if constexpr (CursorType == IDBCursorType::ObjectStore) { + HandleMultipleCursorResponses( + std::move(aResponses), [this](const bool useAsCurrentResult, + ObjectStoreCursorResponse&& response) { + // TODO: Maybe move the deserialization of the clone-read-info into + // the cursor, so that it is only done for records actually accessed, + // which might not be the case for all cached records. + return HandleIndividualCursorResponse( + useAsCurrentResult, std::move(response.key()), + DeserializeStructuredCloneReadInfo( + std::move(response.cloneInfo()), mTransaction->Database(), + PreprocessingNotSupported)); + }); + } + if constexpr (CursorType == IDBCursorType::ObjectStoreKey) { + HandleMultipleCursorResponses( + std::move(aResponses), [this](const bool useAsCurrentResult, + ObjectStoreKeyCursorResponse&& response) { + return HandleIndividualCursorResponse(useAsCurrentResult, + std::move(response.key())); + }); + } + if constexpr (CursorType == IDBCursorType::Index) { + HandleMultipleCursorResponses( + std::move(aResponses), + [this](const bool useAsCurrentResult, IndexCursorResponse&& response) { + return HandleIndividualCursorResponse( + useAsCurrentResult, std::move(response.key()), + std::move(response.sortKey()), std::move(response.objectKey()), + DeserializeStructuredCloneReadInfo( + std::move(response.cloneInfo()), mTransaction->Database(), + PreprocessingNotSupported)); + }); + } + if constexpr (CursorType == IDBCursorType::IndexKey) { + HandleMultipleCursorResponses( + std::move(aResponses), [this](const bool useAsCurrentResult, + IndexKeyCursorResponse&& response) { + return HandleIndividualCursorResponse( + useAsCurrentResult, std::move(response.key()), + std::move(response.sortKey()), std::move(response.objectKey())); + }); + } +} + +template <IDBCursorType CursorType> +void BackgroundCursorChild<CursorType>::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(aWhy == Deletion, !mStrongRequest); + MOZ_ASSERT_IF(aWhy == Deletion, !mStrongCursor); + + MaybeCollectGarbageOnIPCMessage(); + + if (mStrongRequest && !mStrongCursor && mTransaction) { + mTransaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ + aWhy == Deletion); + } + + if (mCursor) { + mCursor->ClearBackgroundActor(); +#ifdef DEBUG + mCursor = nullptr; +#endif + } + +#ifdef DEBUG + mRequest.maybeDestroy(); + mTransaction = Nothing(); + mSource.maybeDestroy(); +#endif +} + +template <IDBCursorType CursorType> +mozilla::ipc::IPCResult BackgroundCursorChild<CursorType>::RecvResponse( + 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(); + + const RefPtr<IDBRequest> request = std::move(mStrongRequest); + Unused << request; // XXX see Bug 1605075 + const RefPtr<IDBCursor> cursor = std::move(mStrongCursor); + Unused << cursor; // XXX see Bug 1605075 + + const auto transaction = + SafeRefPtr{&mTransaction.ref(), AcquireStrongRefFromRawPtr{}}; + + switch (aResponse.type()) { + case CursorResponse::Tnsresult: + HandleResponse(aResponse.get_nsresult()); + break; + + case CursorResponse::Tvoid_t: + HandleResponse(aResponse.get_void_t()); + break; + + case CursorResponse::TArrayOfObjectStoreCursorResponse: + if constexpr (CursorType == IDBCursorType::ObjectStore) { + HandleResponse( + std::move(aResponse.get_ArrayOfObjectStoreCursorResponse())); + } else { + MOZ_CRASH("Response type mismatch"); + } + break; + + case CursorResponse::TArrayOfObjectStoreKeyCursorResponse: + if constexpr (CursorType == IDBCursorType::ObjectStoreKey) { + HandleResponse( + std::move(aResponse.get_ArrayOfObjectStoreKeyCursorResponse())); + } else { + MOZ_CRASH("Response type mismatch"); + } + break; + + case CursorResponse::TArrayOfIndexCursorResponse: + if constexpr (CursorType == IDBCursorType::Index) { + HandleResponse(std::move(aResponse.get_ArrayOfIndexCursorResponse())); + } else { + MOZ_CRASH("Response type mismatch"); + } + break; + + case CursorResponse::TArrayOfIndexKeyCursorResponse: + if constexpr (CursorType == IDBCursorType::IndexKey) { + HandleResponse( + std::move(aResponse.get_ArrayOfIndexKeyCursorResponse())); + } else { + MOZ_CRASH("Response type mismatch"); + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + transaction->OnRequestFinished(/* aRequestCompletedSuccessfully */ true); + + return IPC_OK(); +} + +template class BackgroundCursorChild<IDBCursorType::ObjectStore>; +template class BackgroundCursorChild<IDBCursorType::ObjectStoreKey>; +template class BackgroundCursorChild<IDBCursorType::Index>; +template class BackgroundCursorChild<IDBCursorType::IndexKey>; + +template <typename T> +NS_IMETHODIMP DelayedActionRunnable<T>::Run() { + MOZ_ASSERT(mActor); + mActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + MOZ_ASSERT(mActionFunc); + + ((*mActor).*mActionFunc)(); + + mActor = nullptr; + mRequest = nullptr; + + return NS_OK; +} + +template <typename T> +nsresult DelayedActionRunnable<T>::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) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aManager); + + MOZ_COUNT_CTOR(indexedDB::BackgroundUtilsChild); +} + +BackgroundUtilsChild::~BackgroundUtilsChild() { + MOZ_COUNT_DTOR(indexedDB::BackgroundUtilsChild); +} + +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 dom::indexedDB +} // namespace mozilla diff --git a/dom/indexedDB/ActorsChild.h b/dom/indexedDB/ActorsChild.h new file mode 100644 index 0000000000..693e69cbca --- /dev/null +++ b/dom/indexedDB/ActorsChild.h @@ -0,0 +1,634 @@ +/* -*- 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 "js/RootingAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBCursorType.h" +#include "mozilla/dom/IDBTransaction.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBCursorChild.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseChild.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 "mozilla/InitializedOnce.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +class nsIEventTarget; +struct nsID; + +namespace mozilla { +namespace ipc { + +class BackgroundChildImpl; + +} // namespace ipc + +namespace dom { + +class IDBCursor; +class IDBDatabase; +class IDBFactory; +class IDBOpenDBRequest; +class IDBRequest; +class IndexedDatabaseManager; + +namespace indexedDB { + +class Key; +class PermissionRequestChild; +class PermissionRequestParent; +class SerializedStructuredCloneReadInfo; +struct CloneInfo; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::dom::indexedDB::CloneInfo) + +namespace mozilla::dom::indexedDB { + +class BackgroundFactoryChild final : public PBackgroundIDBFactoryChild { + friend class mozilla::ipc::BackgroundChildImpl; + friend IDBFactory; + + // TODO: This long-lived raw pointer is very suspicious, in particular as it + // is used in BackgroundDatabaseChild::EnsureDOMObject to reacquire a strong + // reference. What ensures it is kept alive, and why can't we store a strong + // reference here? + IDBFactory* mFactory; + + public: + NS_INLINE_DECL_REFCOUNTING(BackgroundFactoryChild, override) + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(BackgroundFactoryChild); + } + + IDBFactory& GetDOMObject() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mFactory); + return *mFactory; + } + + bool SendDeleteMe() = delete; + + private: + // Only created by IDBFactory. + explicit BackgroundFactoryChild(IDBFactory& aFactory); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~BackgroundFactoryChild(); + + void SendDeleteMeInternal(); + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + PBackgroundIDBFactoryRequestChild* AllocPBackgroundIDBFactoryRequestChild( + const FactoryRequestParams& aParams); + + bool DeallocPBackgroundIDBFactoryRequestChild( + PBackgroundIDBFactoryRequestChild* aActor); + + PBackgroundIDBDatabaseChild* AllocPBackgroundIDBDatabaseChild( + const DatabaseSpec& aSpec, + PBackgroundIDBFactoryRequestChild* aRequest) const; + + bool DeallocPBackgroundIDBDatabaseChild(PBackgroundIDBDatabaseChild* aActor); + + mozilla::ipc::IPCResult RecvPBackgroundIDBDatabaseConstructor( + PBackgroundIDBDatabaseChild* aActor, const DatabaseSpec& aSpec, + NotNull<PBackgroundIDBFactoryRequestChild*> aRequest) override; +}; + +class BackgroundDatabaseChild; + +class BackgroundRequestChildBase { + protected: + const NotNull<RefPtr<IDBRequest>> mRequest; + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + protected: + explicit BackgroundRequestChildBase( + MovingNotNull<RefPtr<IDBRequest>> aRequest); + + virtual ~BackgroundRequestChildBase(); +}; + +class BackgroundFactoryRequestChild final + : public BackgroundRequestChildBase, + public PBackgroundIDBFactoryRequestChild { + using PersistenceType = mozilla::dom::quota::PersistenceType; + + friend IDBFactory; + friend class BackgroundFactoryChild; + friend class BackgroundDatabaseChild; + friend class PermissionRequestChild; + friend class PermissionRequestParent; + + const SafeRefPtr<IDBFactory> mFactory; + + // Normally when opening of a database is successful, we receive a database + // actor in request response, so we can use it to call ReleaseDOMObject() + // which clears temporary strong reference to IDBDatabase. + // However, when there's an error, we don't receive a database actor and + // IDBRequest::mTransaction is already cleared (must be). So the only way how + // to call ReleaseDOMObject() is to have a back-reference to database actor. + // This creates a weak ref cycle between + // BackgroundFactoryRequestChild (using mDatabaseActor member) and + // BackgroundDatabaseChild actor (using mOpenRequestActor member). + // mDatabaseActor is set in EnsureDOMObject() and cleared in + // ReleaseDOMObject(). + BackgroundDatabaseChild* mDatabaseActor; + + const uint64_t mRequestedVersion; + const bool mIsDeleteOp; + + public: + NotNull<IDBOpenDBRequest*> GetOpenDBRequest() const; + + private: + // Only created by IDBFactory. + BackgroundFactoryRequestChild( + SafeRefPtr<IDBFactory> aFactory, + MovingNotNull<RefPtr<IDBOpenDBRequest>> aOpenRequest, bool aIsDeleteOp, + uint64_t aRequestedVersion); + + // Only destroyed by BackgroundFactoryChild. + ~BackgroundFactoryRequestChild(); + + void SetDatabaseActor(BackgroundDatabaseChild* aActor); + + void HandleResponse(nsresult aResponse); + + void HandleResponse(const OpenDatabaseRequestResponse& aResponse); + + void HandleResponse(const DeleteDatabaseRequestResponse& aResponse); + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult Recv__delete__( + const FactoryRequestResponse& aResponse); + + mozilla::ipc::IPCResult RecvPermissionChallenge( + PrincipalInfo&& aPrincipalInfo); + + mozilla::ipc::IPCResult RecvBlocked(uint64_t aCurrentVersion); +}; + +class BackgroundDatabaseChild final : public PBackgroundIDBDatabaseChild { + friend class BackgroundFactoryChild; + friend class BackgroundFactoryRequestChild; + friend IDBDatabase; + + UniquePtr<DatabaseSpec> mSpec; + RefPtr<IDBDatabase> mTemporaryStrongDatabase; + BackgroundFactoryRequestChild* mOpenRequestActor; + IDBDatabase* mDatabase; + + public: + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + const DatabaseSpec* Spec() const { + AssertIsOnOwningThread(); + return mSpec.get(); + } + + IDBDatabase* GetDOMObject() const { + AssertIsOnOwningThread(); + return mDatabase; + } + + bool SendDeleteMe() = delete; + + private: + // Only constructed by BackgroundFactoryChild. + BackgroundDatabaseChild(const DatabaseSpec& aSpec, + BackgroundFactoryRequestChild* aOpenRequest); + + // Only destroyed by BackgroundFactoryChild. + ~BackgroundDatabaseChild(); + + void SendDeleteMeInternal(); + + [[nodiscard]] bool EnsureDOMObject(); + + void ReleaseDOMObject(); + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + PBackgroundIDBDatabaseFileChild* AllocPBackgroundIDBDatabaseFileChild( + const IPCBlob& aIPCBlob); + + bool DeallocPBackgroundIDBDatabaseFileChild( + PBackgroundIDBDatabaseFileChild* aActor) const; + + already_AddRefed<PBackgroundIDBVersionChangeTransactionChild> + AllocPBackgroundIDBVersionChangeTransactionChild(uint64_t aCurrentVersion, + uint64_t aRequestedVersion, + int64_t aNextObjectStoreId, + int64_t aNextIndexId); + + mozilla::ipc::IPCResult RecvPBackgroundIDBVersionChangeTransactionConstructor( + PBackgroundIDBVersionChangeTransactionChild* aActor, + const uint64_t& aCurrentVersion, const uint64_t& aRequestedVersion, + const int64_t& aNextObjectStoreId, const int64_t& aNextIndexId) override; + + mozilla::ipc::IPCResult RecvVersionChange(uint64_t aOldVersion, + Maybe<uint64_t> aNewVersion); + + mozilla::ipc::IPCResult RecvInvalidate(); + + mozilla::ipc::IPCResult RecvCloseAfterInvalidationComplete(); +}; + +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. + SafeRefPtr<IDBTransaction> mTemporaryStrongTransaction; + + protected: + // mTransaction is weak and is valid until the NoteActorDestroyed() member + // function is called. + IDBTransaction* mTransaction = nullptr; + + public: +#ifdef DEBUG + virtual void AssertIsOnOwningThread() const = 0; +#else + void AssertIsOnOwningThread() const {} +#endif + + IDBTransaction* GetDOMObject() const { + AssertIsOnOwningThread(); + return mTransaction; + } + + protected: + MOZ_COUNTED_DEFAULT_CTOR(BackgroundTransactionBase); + + explicit BackgroundTransactionBase(SafeRefPtr<IDBTransaction> aTransaction); + + MOZ_COUNTED_DTOR_VIRTUAL(BackgroundTransactionBase); + + void NoteActorDestroyed(); + + void NoteComplete(); + + private: + // Only called by BackgroundVersionChangeTransactionChild. + void SetDOMTransaction(SafeRefPtr<IDBTransaction> aTransaction); +}; + +class BackgroundTransactionChild final : public BackgroundTransactionBase, + public PBackgroundIDBTransactionChild { + friend class BackgroundDatabaseChild; + friend IDBDatabase; + + public: + NS_INLINE_DECL_REFCOUNTING(BackgroundTransactionChild, override) + +#ifdef DEBUG + void AssertIsOnOwningThread() const override; +#endif + + void SendDeleteMeInternal(); + + bool SendDeleteMe() = delete; + + private: + // Only created by IDBDatabase. + explicit BackgroundTransactionChild(SafeRefPtr<IDBTransaction> aTransaction); + + // Only destroyed by BackgroundDatabaseChild. + ~BackgroundTransactionChild(); + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvComplete(nsresult aResult); + + PBackgroundIDBRequestChild* AllocPBackgroundIDBRequestChild( + const RequestParams& aParams); + + bool DeallocPBackgroundIDBRequestChild(PBackgroundIDBRequestChild* aActor); + + PBackgroundIDBCursorChild* AllocPBackgroundIDBCursorChild( + const OpenCursorParams& aParams); + + bool DeallocPBackgroundIDBCursorChild(PBackgroundIDBCursorChild* aActor); +}; + +class BackgroundVersionChangeTransactionChild final + : public BackgroundTransactionBase, + public PBackgroundIDBVersionChangeTransactionChild { + friend class BackgroundDatabaseChild; + + IDBOpenDBRequest* mOpenDBRequest; + + public: + NS_INLINE_DECL_REFCOUNTING(BackgroundVersionChangeTransactionChild, override) + +#ifdef DEBUG + void AssertIsOnOwningThread() const override; +#endif + + void SendDeleteMeInternal(bool aFailedConstructor); + + bool SendDeleteMe() = delete; + + private: + // Only created by BackgroundDatabaseChild. + explicit BackgroundVersionChangeTransactionChild( + IDBOpenDBRequest* aOpenDBRequest); + + // Only destroyed by BackgroundDatabaseChild. + ~BackgroundVersionChangeTransactionChild(); + + // Only called by BackgroundDatabaseChild. + using BackgroundTransactionBase::SetDOMTransaction; + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvComplete(nsresult aResult); + + PBackgroundIDBRequestChild* AllocPBackgroundIDBRequestChild( + const RequestParams& aParams); + + bool DeallocPBackgroundIDBRequestChild(PBackgroundIDBRequestChild* aActor); + + PBackgroundIDBCursorChild* AllocPBackgroundIDBCursorChild( + const OpenCursorParams& aParams); + + bool DeallocPBackgroundIDBCursorChild(PBackgroundIDBCursorChild* aActor); +}; + +class BackgroundRequestChild final : public BackgroundRequestChildBase, + public PBackgroundIDBRequestChild { + friend class BackgroundTransactionChild; + friend class BackgroundVersionChangeTransactionChild; + friend struct CloneInfo; + friend IDBTransaction; + + class PreprocessHelper; + + SafeRefPtr<IDBTransaction> mTransaction; + nsTArray<CloneInfo> mCloneInfos; + uint32_t mRunningPreprocessHelpers; + uint32_t mCurrentCloneDataIndex; + nsresult mPreprocessResultCode; + bool mGetAll; + + private: + // Only created by IDBTransaction. + explicit BackgroundRequestChild(MovingNotNull<RefPtr<IDBRequest>> aRequest); + + // Only destroyed by BackgroundTransactionChild or + // BackgroundVersionChangeTransactionChild. + ~BackgroundRequestChild(); + + void MaybeSendContinue(); + + void OnPreprocessFinished(uint32_t aCloneDataIndex, + UniquePtr<JSStructuredCloneData> aCloneData); + + void OnPreprocessFailed(uint32_t aCloneDataIndex, nsresult aErrorCode); + + UniquePtr<JSStructuredCloneData> GetNextCloneData(); + + void HandleResponse(nsresult aResponse); + + void HandleResponse(const Key& aResponse); + + void HandleResponse(const nsTArray<Key>& aResponse); + + void HandleResponse(SerializedStructuredCloneReadInfo&& aResponse); + + void HandleResponse(nsTArray<SerializedStructuredCloneReadInfo>&& aResponse); + + void HandleResponse(JS::Handle<JS::Value> aResponse); + + void HandleResponse(uint64_t aResponse); + + nsresult HandlePreprocess(const PreprocessInfo& aPreprocessInfo); + + nsresult HandlePreprocess(const nsTArray<PreprocessInfo>& aPreprocessInfos); + + nsresult HandlePreprocessInternal( + const nsTArray<PreprocessInfo>& aPreprocessInfos); + + SafeRefPtr<IDBTransaction> AcquireTransaction() const { + return mTransaction.clonePtr(); + } + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult Recv__delete__(RequestResponse&& aResponse); + + mozilla::ipc::IPCResult RecvPreprocess(const PreprocessParams& aParams); +}; + +struct CloneInfo { + RefPtr<BackgroundRequestChild::PreprocessHelper> mPreprocessHelper; + UniquePtr<JSStructuredCloneData> mCloneData; +}; + +class BackgroundCursorChildBase + : public PBackgroundIDBCursorChild, + public SafeRefCounted<BackgroundCursorChildBase> { + private: + NS_DECL_OWNINGTHREAD + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME( + mozilla::dom::indexedDB::BackgroundCursorChildBase) + MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(BackgroundCursorChildBase, + SafeRefCounted) + + protected: + ~BackgroundCursorChildBase(); + + InitializedOnce<const NotNull<IDBRequest*>> mRequest; + Maybe<IDBTransaction&> mTransaction; + + // These are only set while a request is in progress. + RefPtr<IDBRequest> mStrongRequest; + RefPtr<IDBCursor> mStrongCursor; + + const Direction mDirection; + + BackgroundCursorChildBase(NotNull<IDBRequest*> aRequest, + Direction aDirection); + + void HandleResponse(nsresult aResponse); + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(BackgroundCursorChildBase); + } + + MovingNotNull<RefPtr<IDBRequest>> AcquireRequest() const; + + NotNull<IDBRequest*> GetRequest() const { + AssertIsOnOwningThread(); + + return *mRequest; + } + + Direction GetDirection() const { + AssertIsOnOwningThread(); + + return mDirection; + } + + virtual void SendDeleteMeInternal() = 0; + + virtual mozilla::ipc::IPCResult RecvResponse(CursorResponse&& aResponse) = 0; +}; + +template <IDBCursorType CursorType> +class BackgroundCursorChild final : public BackgroundCursorChildBase { + public: + using SourceType = CursorSourceType<CursorType>; + using ResponseType = typename CursorTypeTraits<CursorType>::ResponseType; + + private: + friend class BackgroundTransactionChild; + friend class BackgroundVersionChangeTransactionChild; + + InitializedOnce<const NotNull<SourceType*>> mSource; + IDBCursorImpl<CursorType>* mCursor; + + std::deque<CursorData<CursorType>> mCachedResponses, mDelayedResponses; + bool mInFlightResponseInvalidationNeeded; + + public: + BackgroundCursorChild(NotNull<IDBRequest*> aRequest, SourceType* aSource, + Direction aDirection); + + void SendContinueInternal(const CursorRequestParams& aParams, + const CursorData<CursorType>& aCurrentData); + + void InvalidateCachedResponses(); + + template <typename Condition> + void DiscardCachedResponses(const Condition& aConditionFunc); + + SourceType* GetSource() const { + AssertIsOnOwningThread(); + + return *mSource; + } + + void SendDeleteMeInternal() final; + + private: + // Only destroyed by BackgroundTransactionChild or + // BackgroundVersionChangeTransactionChild. + ~BackgroundCursorChild(); + + void CompleteContinueRequestFromCache(); + + using BackgroundCursorChildBase::HandleResponse; + + void HandleResponse(const void_t& aResponse); + + void HandleResponse(nsTArray<ResponseType>&& aResponses); + + template <typename Func> + void HandleMultipleCursorResponses(nsTArray<ResponseType>&& aResponses, + const Func& aHandleRecord); + + template <typename... Args> + [[nodiscard]] RefPtr<IDBCursor> HandleIndividualCursorResponse( + bool aUseAsCurrentResult, Args&&... aArgs); + + SafeRefPtr<BackgroundCursorChild> SafeRefPtrFromThis(); + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvResponse(CursorResponse&& aResponse) override; + + // Force callers to use SendContinueInternal. + bool SendContinue(const CursorRequestParams& aParams, const Key& aCurrentKey, + const Key& aCurrentObjectStoreKey) = delete; + + bool SendDeleteMe() = delete; +}; + +class BackgroundUtilsChild final : public PBackgroundIndexedDBUtilsChild { + friend class mozilla::ipc::BackgroundChildImpl; + friend IndexedDatabaseManager; + + IndexedDatabaseManager* mManager; + + NS_DECL_OWNINGTHREAD + + public: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(BackgroundUtilsChild); + } + + bool SendDeleteMe() = delete; + + private: + // Only created by IndexedDatabaseManager. + explicit BackgroundUtilsChild(IndexedDatabaseManager* aManager); + + // Only destroyed by mozilla::ipc::BackgroundChildImpl. + ~BackgroundUtilsChild(); + + void SendDeleteMeInternal(); + + public: + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +} // namespace mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_actorschild_h__ diff --git a/dom/indexedDB/ActorsParent.cpp b/dom/indexedDB/ActorsParent.cpp new file mode 100644 index 0000000000..1d0a18f336 --- /dev/null +++ b/dom/indexedDB/ActorsParent.cpp @@ -0,0 +1,20861 @@ +/* -*- 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 "ActorsParent.h" + +#include <inttypes.h> +#include <math.h> +#include <stdlib.h> +#include <string.h> +#include <algorithm> +#include <cstdint> +#include <functional> +#include <iterator> +#include <new> +#include <numeric> +#include <tuple> +#include <type_traits> +#include <utility> +#include "ActorsParentCommon.h" +#include "CrashAnnotations.h" +#include "DatabaseFileInfo.h" +#include "DatabaseFileManager.h" +#include "DatabaseFileManagerImpl.h" +#include "DBSchema.h" +#include "ErrorList.h" +#include "IDBCursorType.h" +#include "IDBObjectStore.h" +#include "IDBTransaction.h" +#include "IndexedDBCommon.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDatabaseManager.h" +#include "IndexedDBCipherKeyManager.h" +#include "KeyPath.h" +#include "MainThreadUtils.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "SafeRefPtr.h" +#include "SchemaUpgrades.h" +#include "chrome/common/ipc_channel.h" +#include "ipc/IPCMessageUtils.h" +#include "js/RootingAPI.h" +#include "js/StructuredClone.h" +#include "js/Value.h" +#include "jsapi.h" +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageConnection.h" +#include "mozIStorageFunction.h" +#include "mozIStorageProgressHandler.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozilla/Algorithm.h" +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/ArrayIterator.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/Casting.h" +#include "mozilla/CondVar.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/Logging.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/Monitor.h" +#include "mozilla/Mutex.h" +#include "mozilla/NotNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefCountType.h" +#include "mozilla/RefCounted.h" +#include "mozilla/RemoteLazyInputStreamParent.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Scoped.h" +#include "mozilla/SnappyCompressOutputStream.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TaskCategory.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IPCBlob.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/IndexedDatabase.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/PContentParent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/indexedDB/IDBResult.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBCursor.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabase.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactory.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBRequest.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h" +#include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/CachingDatabaseConnection.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/DecryptingInputStream_impl.h" +#include "mozilla/dom/quota/EncryptingOutputStream_impl.h" +#include "mozilla/dom/quota/FileStreams.h" +#include "mozilla/dom/quota/OriginScope.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/fallible.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/mozalloc.h" +#include "mozilla/storage/Variant.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsContentUtils.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsEscape.h" +#include "nsHashKeys.h" +#include "nsIAsyncInputStream.h" +#include "nsID.h" +#include "nsIDUtils.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsIFileProtocolHandler.h" +#include "nsIFileStreams.h" +#include "nsIFileURL.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIProtocolHandler.h" +#include "nsIRunnable.h" +#include "nsISupports.h" +#include "nsISupportsPriority.h" +#include "nsISupportsUtils.h" +#include "nsIThread.h" +#include "nsIThreadInternal.h" +#include "nsITimer.h" +#include "nsIURIMutator.h" +#include "nsIVariant.h" +#include "nsLiteralString.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTHashSet.h" +#include "nsTHashtable.h" +#include "nsTLiteralString.h" +#include "nsTStringRepr.h" +#include "nsThreadPool.h" +#include "nsThreadUtils.h" +#include "nscore.h" +#include "prinrval.h" +#include "prio.h" +#include "prsystem.h" +#include "prthread.h" +#include "prtime.h" +#include "prtypes.h" +#include "snappy/snappy.h" + +struct JSContext; +class JSObject; +template <class T> +class nsPtrHashKey; + +#define IDB_DEBUG_LOG(_args) \ + MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), LogLevel::Debug, _args) + +#if defined(MOZ_WIDGET_ANDROID) +# define IDB_MOBILE +#endif + +// Helper macros to reduce assertion verbosity +// AUUF == ASSERT_UNREACHABLE_UNLESS_FUZZING +#ifdef DEBUG +# ifdef FUZZING +# define NS_AUUF_OR_WARN(...) NS_WARNING(__VA_ARGS__) +# else +# define NS_AUUF_OR_WARN(...) MOZ_ASSERT(false, __VA_ARGS__) +# endif +# define NS_AUUF_OR_WARN_IF(cond) \ + [](bool aCond) { \ + if (MOZ_UNLIKELY(aCond)) { \ + NS_AUUF_OR_WARN(#cond); \ + } \ + return aCond; \ + }((cond)) +#else +# define NS_AUUF_OR_WARN(...) \ + do { \ + } while (false) +# define NS_AUUF_OR_WARN_IF(cond) static_cast<bool>(cond) +#endif + +namespace mozilla { + +namespace dom::indexedDB { + +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; +using mozilla::dom::quota::Client; + +namespace { + +class ConnectionPool; +class Database; +struct DatabaseActorInfo; +class DatabaseFile; +class DatabaseLoggingInfo; +class DatabaseMaintenance; +class Factory; +class Maintenance; +class OpenDatabaseOp; +class TransactionBase; +class TransactionDatabaseOperationBase; +class VersionChangeTransaction; +template <bool StatementHasIndexKeyBindings> +struct ValuePopulateResponseHelper; + +/******************************************************************************* + * Constants + ******************************************************************************/ + +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;"_ns + +// For efficiency reasons, kEncryptedStreamBlockSize must be a multiple of large +// 4k disk sectors. +static_assert(kEncryptedStreamBlockSize % 4096 == 0); +// Similarly, the file copy buffer size must be a multiple of the encrypted +// block size. +static_assert(kFileCopyBufferSize % kEncryptedStreamBlockSize == 0); + +constexpr auto kFileManagerDirectoryNameSuffix = u".files"_ns; +constexpr auto kSQLiteSuffix = u".sqlite"_ns; +constexpr auto kSQLiteJournalSuffix = u".sqlite-journal"_ns; +constexpr auto kSQLiteSHMSuffix = u".sqlite-shm"_ns; +constexpr auto kSQLiteWALSuffix = u".sqlite-wal"_ns; + +constexpr auto kPermissionStringBase = "indexedDB-chrome-"_ns; +constexpr auto kPermissionReadSuffix = "-read"_ns; +constexpr auto kPermissionWriteSuffix = "-write"_ns; + +// The following constants define all names of binding parameters in statements, +// where they are bound by name. This should include all parameter names which +// are bound by name. Binding may be done by index when the statement definition +// and binding are done in the same local scope, and no other reasons prevent +// using the indexes (e.g. multiple statement variants with differing number or +// order of parameters). Neither the styles of specifying parameter names +// (literally vs. via these constants) nor the binding styles (by index vs. by +// name) should not be mixed for the same statement. The decision must be made +// for each statement based on the proximity of statement and binding calls. +constexpr auto kStmtParamNameCurrentKey = "current_key"_ns; +constexpr auto kStmtParamNameRangeBound = "range_bound"_ns; +constexpr auto kStmtParamNameObjectStorePosition = "object_store_position"_ns; +constexpr auto kStmtParamNameLowerKey = "lower_key"_ns; +constexpr auto kStmtParamNameUpperKey = "upper_key"_ns; +constexpr auto kStmtParamNameKey = "key"_ns; +constexpr auto kStmtParamNameObjectStoreId = "object_store_id"_ns; +constexpr auto kStmtParamNameIndexId = "index_id"_ns; +// TODO: Maybe the uses of kStmtParamNameId should be replaced by more +// specific constants such as kStmtParamNameObjectStoreId. +constexpr auto kStmtParamNameId = "id"_ns; +constexpr auto kStmtParamNameValue = "value"_ns; +constexpr auto kStmtParamNameObjectDataKey = "object_data_key"_ns; +constexpr auto kStmtParamNameIndexDataValues = "index_data_values"_ns; +constexpr auto kStmtParamNameData = "data"_ns; +constexpr auto kStmtParamNameFileIds = "file_ids"_ns; +constexpr auto kStmtParamNameValueLocale = "value_locale"_ns; +constexpr auto kStmtParamNameLimit = "limit"_ns; + +// The following constants define some names of columns in tables, which are +// referred to in remote locations, e.g. in calls to +// GetBindingClauseForKeyRange. +constexpr auto kColumnNameKey = "key"_ns; +constexpr auto kColumnNameValue = "value"_ns; +constexpr auto kColumnNameAliasSortKey = "sort_column"_ns; + +// SQL fragments used at multiple locations. +constexpr auto kOpenLimit = " LIMIT "_ns; + +// The deletion marker file is created before RemoveDatabaseFilesAndDirectory +// begins deleting a database. It is removed as the last step of deletion. If a +// deletion marker file is found when initializing the origin, the deletion +// routine is run again to ensure that the database and all of its related files +// are removed. The primary goal of this mechanism is to avoid situations where +// a database has been partially deleted, leading to inconsistent state for the +// origin. +constexpr auto kIdbDeletionMarkerFilePrefix = u"idb-deleting-"_ns; + +const uint32_t kDeleteTimeoutMs = 1000; + +#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 + +/******************************************************************************* + * Metadata classes + ******************************************************************************/ + +// Can be instantiated either on the QuotaManager IO thread or on a +// versionchange transaction thread. These threads can never race so this is +// totally safe. +struct FullIndexMetadata { + IndexMetadata mCommonMetadata = {0, nsString(), KeyPath(0), nsCString(), + false, false, false}; + + FlippedOnce<false> mDeleted; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata) + + private: + ~FullIndexMetadata() = default; +}; + +using IndexTable = nsTHashMap<nsUint64HashKey, SafeRefPtr<FullIndexMetadata>>; + +// Can be instantiated either on the QuotaManager IO thread or on a +// versionchange transaction thread. These threads can never race so this is +// totally safe. +struct FullObjectStoreMetadata { + ObjectStoreMetadata mCommonMetadata; + IndexTable mIndexes; + + // The auto increment ids are touched on both the background thread and the + // transaction I/O thread, and they must be kept in sync, so we need a mutex + // to protect them. + struct AutoIncrementIds { + int64_t next; + int64_t committed; + }; + DataMutex<AutoIncrementIds> mAutoIncrementIds; + + FlippedOnce<false> mDeleted; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata); + + bool HasLiveIndexes() const; + + FullObjectStoreMetadata(ObjectStoreMetadata aCommonMetadata, + const AutoIncrementIds& aAutoIncrementIds) + : mCommonMetadata{std::move(aCommonMetadata)}, + mAutoIncrementIds{AutoIncrementIds{aAutoIncrementIds}, + "FullObjectStoreMetadata"} {} + + private: + ~FullObjectStoreMetadata() = default; +}; + +using ObjectStoreTable = + nsTHashMap<nsUint64HashKey, SafeRefPtr<FullObjectStoreMetadata>>; + +static_assert( + std::is_same_v<IndexOrObjectStoreId, + std::remove_cv_t<std::remove_reference_t< + decltype(std::declval<const ObjectStoreGetParams&>() + .objectStoreId())>>>); +static_assert( + std::is_same_v< + IndexOrObjectStoreId, + std::remove_cv_t<std::remove_reference_t< + decltype(std::declval<const IndexGetParams&>().objectStoreId())>>>); + +struct FullDatabaseMetadata final : AtomicSafeRefCounted<FullDatabaseMetadata> { + DatabaseMetadata mCommonMetadata; + nsCString mDatabaseId; + nsString mFilePath; + ObjectStoreTable mObjectStores; + + IndexOrObjectStoreId mNextObjectStoreId = 0; + IndexOrObjectStoreId mNextIndexId = 0; + + public: + explicit FullDatabaseMetadata(const DatabaseMetadata& aCommonMetadata) + : mCommonMetadata(aCommonMetadata) { + AssertIsOnBackgroundThread(); + } + + [[nodiscard]] SafeRefPtr<FullDatabaseMetadata> Duplicate() const; + + MOZ_DECLARE_REFCOUNTED_TYPENAME(FullDatabaseMetadata) +}; + +template <class Enumerable> +auto MatchMetadataNameOrId(const Enumerable& aEnumerable, + IndexOrObjectStoreId aId, + Maybe<const nsAString&> aName = Nothing()) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aId); + + const auto it = std::find_if( + aEnumerable.cbegin(), aEnumerable.cend(), + [aId, aName](const auto& entry) { + MOZ_ASSERT(entry.GetKey() != 0); + + const auto& value = entry.GetData(); + MOZ_ASSERT(value); + + return !value->mDeleted && + (aId == value->mCommonMetadata.id() || + (aName && *aName == value->mCommonMetadata.name())); + }); + + return ToMaybeRef(it != aEnumerable.cend() ? it->GetData().unsafeGetRawPtr() + : nullptr); +} + +/******************************************************************************* + * SQLite functions + ******************************************************************************/ + +// WARNING: the hash function used for the database name must not change. +// That's why this function exists separately from mozilla::HashString(), even +// though it is (at the time of writing) equivalent. See bug 780408 and bug +// 940315 for details. +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; + + return std::accumulate(aName.BeginReading(), aName.EndReading(), uint32_t(0), + [](uint32_t hash, char16_t ch) { + return kGoldenRatioU32 * + (Helper::RotateBitsLeft32(hash, 5) ^ ch); + }); +} + +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%" PRIX32 + ") to " + "NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR", + static_cast<uint32_t>(aResultCode)); + NS_WARNING(message.get()); +#else + ; +#endif + } + + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; +} + +Result<nsCOMPtr<nsIFileURL>, nsresult> GetDatabaseFileURL( + nsIFile& aDatabaseFile, const int64_t aDirectoryLockId, + const Maybe<CipherKey>& aMaybeKey) { + MOZ_ASSERT(aDirectoryLockId >= -1); + + QM_TRY_INSPECT( + const auto& protocolHandler, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIProtocolHandler>, + MOZ_SELECT_OVERLOAD(do_GetService), + NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file")); + + QM_TRY_INSPECT(const auto& fileHandler, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIFileProtocolHandler>, + MOZ_SELECT_OVERLOAD(do_QueryInterface), + protocolHandler)); + + QM_TRY_INSPECT(const auto& mutator, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIURIMutator>, fileHandler, + NewFileURIMutator, &aDatabaseFile)); + + // aDirectoryLockId should only be -1 when we are called + // - from DatabaseFileManager::InitDirectory when the temporary storage + // hasn't been initialized yet. At that time, the in-memory objects (e.g. + // OriginInfo) are only being created so it doesn't make sense to tunnel + // quota information to QuotaVFS to get corresponding QuotaObject instances + // for SQLite files. + // - from DeleteDatabaseOp::LoadPreviousVersion, since this might require + // temporarily exceeding the quota limit before the database can be + // deleted. + const auto directoryLockIdClause = + aDirectoryLockId >= 0 + ? "&directoryLockId="_ns + IntToCString(aDirectoryLockId) + : EmptyCString(); + + const auto keyClause = [&aMaybeKey] { + nsAutoCString keyClause; + if (aMaybeKey) { + keyClause.AssignLiteral("&key="); + for (uint8_t byte : IndexedDBCipherStrategy::SerializeKey(*aMaybeKey)) { + keyClause.AppendPrintf("%02x", byte); + } + } + return keyClause; + }(); + + QM_TRY_UNWRAP(auto result, ([&mutator, &directoryLockIdClause, &keyClause] { + nsCOMPtr<nsIFileURL> result; + nsresult rv = NS_MutateURI(mutator) + .SetQuery("cache=private"_ns + + directoryLockIdClause + keyClause) + .Finalize(result); + return NS_SUCCEEDED(rv) + ? Result<nsCOMPtr<nsIFileURL>, nsresult>{result} + : Err(rv); + }())); + + return result; +} + +nsresult SetDefaultPragmas(mozIStorageConnection& aConnection) { + MOZ_ASSERT(!NS_IsMainThread()); + + static constexpr auto 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;"_ns; + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(kBuiltInPragmas))); + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsAutoCString{ + "PRAGMA synchronous = "_ns + + (IndexedDatabaseManager::FullSynchronous() ? "FULL"_ns : "NORMAL"_ns) + + ";"_ns}))); + +#ifndef IDB_MOBILE + if (kSQLiteGrowthIncrement) { + // This is just an optimization so ignore the failure if the disk is + // currently too full. + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT( + aConnection.SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns)), + // Predicate. + IsSpecificError<NS_ERROR_FILE_TOO_BIG>, + // Fallback. + ErrToDefaultOk<>)); + } +#endif // IDB_MOBILE + + return NS_OK; +} + +nsresult SetJournalMode(mozIStorageConnection& aConnection) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Try enabling WAL mode. This can fail in various circumstances so we have to + // check the results here. + constexpr auto journalModeQueryStart = "PRAGMA journal_mode = "_ns; + constexpr auto journalModeWAL = "wal"_ns; + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, journalModeQueryStart + journalModeWAL)); + + QM_TRY_INSPECT( + const auto& journalMode, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, *stmt, GetUTF8String, 0)); + + if (journalMode.Equals(journalModeWAL)) { + // WAL mode successfully enabled. Maybe set limits on its size here. + if (kMaxWALPages >= 0) { + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + "PRAGMA wal_autocheckpoint = "_ns + IntToCString(kMaxWALPages)))); + } + } else { + NS_WARNING("Failed to set WAL mode, falling back to normal journal mode."); +#ifdef IDB_MOBILE + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns))); +#endif + } + + return NS_OK; +} + +Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> OpenDatabase( + mozIStorageService& aStorageService, nsIFileURL& aFileURL, + const uint32_t aTelemetryId = 0) { + const nsAutoCString telemetryFilename = + aTelemetryId ? "indexedDB-"_ns + IntToCString(aTelemetryId) + + NS_ConvertUTF16toUTF8(kSQLiteSuffix) + : nsAutoCString(); + + QM_TRY_UNWRAP(auto connection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, aStorageService, + OpenDatabaseWithFileURL, &aFileURL, telemetryFilename, + mozIStorageService::CONNECTION_DEFAULT)); + + return WrapMovingNotNull(std::move(connection)); +} + +Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> +OpenDatabaseAndHandleBusy(mozIStorageService& aStorageService, + nsIFileURL& aFileURL, + const uint32_t aTelemetryId = 0) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + using ConnectionType = Maybe<MovingNotNull<nsCOMPtr<mozIStorageConnection>>>; + + QM_TRY_UNWRAP(auto connection, + QM_OR_ELSE_WARN_IF( + // Expression + OpenDatabase(aStorageService, aFileURL, aTelemetryId) + .map([](auto connection) -> ConnectionType { + return Some(std::move(connection)); + }), + // Predicate. + IsSpecificError<NS_ERROR_STORAGE_BUSY>, + // Fallback. + ErrToDefaultOk<ConnectionType>)); + + if (connection.isNothing()) { +#ifdef DEBUG + { + nsCString path; + MOZ_ALWAYS_SUCCEEDS(aFileURL.GetFileName(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. + const TimeStamp start = TimeStamp::NowLoRes(); + + do { + PR_Sleep(PR_MillisecondsToInterval(100)); + + QM_TRY_UNWRAP(connection, + QM_OR_ELSE_WARN_IF( + // Expression. + OpenDatabase(aStorageService, aFileURL, aTelemetryId) + .map([](auto connection) -> ConnectionType { + return Some(std::move(connection)); + }), + // Predicate. + ([&start](nsresult aValue) { + return aValue == NS_ERROR_STORAGE_BUSY && + TimeStamp::NowLoRes() - start <= + TimeDuration::FromSeconds(10); + }), + // Fallback. + ErrToDefaultOk<ConnectionType>)); + } while (connection.isNothing()); + } + + return connection.extract(); +} + +// Returns true if a given nsIFile exists and is a directory. Returns false if +// it doesn't exist. Returns an error if it exists, but is not a directory, or +// any other error occurs. +Result<bool, nsresult> ExistsAsDirectory(nsIFile& aDirectory) { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE)); + } + + return exists; +} + +constexpr nsresult mapNoDeviceSpaceError(nsresult aRv) { + if (aRv == 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. + return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR; + } + return aRv; +} + +Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> +CreateStorageConnection(nsIFile& aDBFile, nsIFile& aFMDirectory, + const nsAString& aName, const nsACString& aOrigin, + const int64_t aDirectoryLockId, + const uint32_t aTelemetryId, + const Maybe<CipherKey>& aMaybeKey) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectoryLockId >= -1); + + AUTO_PROFILER_LABEL("CreateStorageConnection", DOM); + + QM_TRY_INSPECT(const auto& dbFileUrl, + GetDatabaseFileURL(aDBFile, aDirectoryLockId, aMaybeKey)); + + QM_TRY_INSPECT(const auto& storageService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP( + auto connection, + QM_OR_ELSE_WARN_IF( + // Expression. + OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId) + .map([](auto connection) -> nsCOMPtr<mozIStorageConnection> { + return std::move(connection).unwrapBasePtr(); + }), + // Predicate. + ([&aName](nsresult aValue) { + // 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. + return IsDatabaseCorruptionError(aValue) && !aName.IsVoid(); + }), + // Fallback. + ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>)); + + if (!connection) { + // XXX Shouldn't we also update quota usage? + + // Nuke the database file. + QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(false))); + QM_TRY_INSPECT(const bool& existsAsDirectory, + ExistsAsDirectory(aFMDirectory)); + + if (existsAsDirectory) { + QM_TRY(MOZ_TO_RESULT(aFMDirectory.Remove(true))); + } + + QM_TRY_UNWRAP(connection, OpenDatabaseAndHandleBusy( + *storageService, *dbFileUrl, aTelemetryId)); + } + + QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(*connection))); + QM_TRY(MOZ_TO_RESULT(connection->EnableModule("filesystem"_ns))); + + // Check to make sure that the database schema is correct. + QM_TRY_INSPECT(const int32_t& schemaVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion)); + + // Unknown schema will fail origin initialization too. + QM_TRY(OkIf(schemaVersion || !aName.IsVoid()), + Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](const auto&) { + IDB_WARNING("Unable to open IndexedDB database, schema is not set!"); + }); + + QM_TRY( + OkIf(schemaVersion <= kSQLiteSchemaVersion), + Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), [](const auto&) { + IDB_WARNING("Unable to open IndexedDB database, schema is too high!"); + }); + + bool journalModeSet = false; + + if (schemaVersion != kSQLiteSchemaVersion) { + const bool newDatabase = !schemaVersion; + + if (newDatabase) { + // Set the page size first. + const auto sqlitePageSizeOverride = + aMaybeKey ? 8192 : kSQLitePageSizeOverride; + if (sqlitePageSizeOverride) { + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", sqlitePageSizeOverride)))); + } + + // We have to set the auto_vacuum mode before opening a transaction. + QM_TRY((MOZ_TO_RESULT_INVOKE_MEMBER( + 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). + "PRAGMA auto_vacuum = FULL;"_ns +#else + // Turn on incremental auto_vacuum mode on desktop builds. + "PRAGMA auto_vacuum = INCREMENTAL;"_ns +#endif + ) + .mapErr(mapNoDeviceSpaceError))); + + QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection))); + + 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("PRAGMA foreign_keys = OFF;"_ns)); +#endif + } + + bool vacuumNeeded = false; + + mozStorageTransaction transaction( + connection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + if (newDatabase) { + QM_TRY(MOZ_TO_RESULT(CreateTables(*connection))); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const int32_t& schemaVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } +#endif + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "INSERT INTO database (name, origin) " + "VALUES (:name, :origin)"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindStringByIndex(0, aName))); + QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByIndex(1, aOrigin))); + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + } else { + QM_TRY_UNWRAP(vacuumNeeded, MaybeUpgradeSchema(*connection, schemaVersion, + aFMDirectory, aOrigin)); + } + + QM_TRY(MOZ_TO_RESULT_INVOKE_MEMBER(transaction, Commit) + .mapErr(mapNoDeviceSpaceError)); + +#ifdef DEBUG + if (!newDatabase) { + // Re-enable foreign key support after doing a foreign key check. + QM_TRY_INSPECT(const bool& foreignKeyError, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *connection, "PRAGMA foreign_key_check;"_ns), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(!foreignKeyError, "Database has inconsisistent foreign keys!"); + + MOZ_ALWAYS_SUCCEEDS( + connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns)); + } +#endif + + if (kSQLitePageSizeOverride && !newDatabase) { + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + *connection, "PRAGMA page_size;"_ns)); + + QM_TRY_INSPECT(const int32_t& pageSize, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); + MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); + + if (kSQLitePageSizeOverride != uint32_t(pageSize)) { + // We must not be in WAL journal mode to change the page size. + QM_TRY(MOZ_TO_RESULT( + connection->ExecuteSimpleSQL("PRAGMA journal_mode = DELETE;"_ns))); + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + *connection, "PRAGMA journal_mode;"_ns)); + + QM_TRY_INSPECT(const auto& journalMode, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, *stmt, + GetUTF8String, 0)); + + if (journalMode.EqualsLiteral("delete")) { + // Successfully set to rollback journal mode so changing the page size + // is possible with a VACUUM. + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)))); + + // 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) { + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("VACUUM;"_ns))); + } + + if (newDatabase || vacuumNeeded) { + if (journalModeSet) { + // Make sure we checkpoint to get an accurate file size. + QM_TRY(MOZ_TO_RESULT( + connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(FULL);"_ns))); + } + + QM_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize)); + MOZ_ASSERT(fileSize > 0); + + PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT( + const auto& vacuumTimeStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<mozIStorageStatement>, + connection, CreateStatement, + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;"_ns)); + + QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByIndex(0, vacuumTime))); + QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByIndex(1, fileSize))); + QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute())); + } + } + + if (!journalModeSet) { + QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection))); + } + + return WrapMovingNotNullUnchecked(std::move(connection)); +} + +nsCOMPtr<nsIFile> GetFileForPath(const nsAString& aPath) { + MOZ_ASSERT(!aPath.IsEmpty()); + + QM_TRY_RETURN(QM_NewLocalFile(aPath), nullptr); +} + +Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> +GetStorageConnection(nsIFile& aDatabaseFile, const int64_t aDirectoryLockId, + const uint32_t aTelemetryId, + const Maybe<CipherKey>& aMaybeKey) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aDirectoryLockId >= 0); + + AUTO_PROFILER_LABEL("GetStorageConnection", DOM); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, Exists)); + + QM_TRY(OkIf(exists), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + QM_TRY_INSPECT( + const auto& dbFileUrl, + GetDatabaseFileURL(aDatabaseFile, aDirectoryLockId, aMaybeKey)); + + QM_TRY_INSPECT(const auto& storageService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP( + nsCOMPtr<mozIStorageConnection> connection, + OpenDatabaseAndHandleBusy(*storageService, *dbFileUrl, aTelemetryId)); + + QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(*connection))); + + QM_TRY(MOZ_TO_RESULT(SetJournalMode(*connection))); + + return WrapMovingNotNullUnchecked(std::move(connection)); +} + +Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> +GetStorageConnection(const nsAString& aDatabaseFilePath, + const int64_t aDirectoryLockId, + const uint32_t aTelemetryId, + const Maybe<CipherKey>& aMaybeKey) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, kSQLiteSuffix)); + MOZ_ASSERT(aDirectoryLockId >= 0); + + nsCOMPtr<nsIFile> dbFile = GetFileForPath(aDatabaseFilePath); + + QM_TRY(OkIf(dbFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + return GetStorageConnection(*dbFile, aDirectoryLockId, aTelemetryId, + aMaybeKey); +} + +/******************************************************************************* + * ConnectionPool declarations + ******************************************************************************/ + +class DatabaseConnection final : public CachingDatabaseConnection { + friend class ConnectionPool; + + enum class CheckpointMode { Full, Restart, Truncate }; + + public: + class AutoSavepoint; + class UpdateRefcountFunction; + + private: + InitializedOnce<const NotNull<SafeRefPtr<DatabaseFileManager>>> mFileManager; + RefPtr<UpdateRefcountFunction> mUpdateRefcountFunction; + RefPtr<QuotaObject> mQuotaObject; + RefPtr<QuotaObject> mJournalQuotaObject; + bool mInReadTransaction; + bool mInWriteTransaction; + +#ifdef DEBUG + uint32_t mDEBUGSavepointCount; +#endif + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection) + + UpdateRefcountFunction* GetUpdateRefcountFunction() const { + AssertIsOnConnectionThread(); + + return mUpdateRefcountFunction; + } + + 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( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection, + MovingNotNull<SafeRefPtr<DatabaseFileManager>> aFileManager); + + ~DatabaseConnection(); + + nsresult Init(); + + nsresult CheckpointInternal(CheckpointMode aMode); + + Result<uint32_t, nsresult> GetFreelistCount( + CachedStatement& aCachedStatement); + + /** + * On success, returns whether some pages were freed. + */ + Result<bool, nsresult> ReclaimFreePagesWhileIdle( + CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement, + uint32_t aFreelistCount, bool aNeedsCheckpoint); + + Result<int64_t, nsresult> GetFileSize(const nsAString& aPath); +}; + +class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final { + DatabaseConnection* mConnection; +#ifdef DEBUG + const TransactionBase* mDEBUGTransaction; +#endif + + public: + AutoSavepoint(); + ~AutoSavepoint(); + + nsresult Start(const TransactionBase& aTransaction); + + nsresult Commit(); +}; + +class DatabaseConnection::UpdateRefcountFunction final + : public mozIStorageFunction { + class FileInfoEntry; + + enum class UpdateType { Increment, Decrement }; + + DatabaseConnection* const mConnection; + DatabaseFileManager& mFileManager; + nsClassHashtable<nsUint64HashKey, FileInfoEntry> mFileInfoEntries; + nsTHashMap<nsUint64HashKey, NotNull<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, + DatabaseFileManager& aFileManager); + + nsresult WillCommit(); + + void DidCommit(); + + void DidAbort(); + + void StartSavepoint(); + + void ReleaseSavepoint(); + + void RollbackSavepoint(); + + void Reset(); + + private: + ~UpdateRefcountFunction() = default; + + nsresult ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex, + UpdateType aUpdateType); + + nsresult CreateJournals(); + + nsresult RemoveJournals(const nsTArray<int64_t>& aJournals); +}; + +class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final { + SafeRefPtr<DatabaseFileInfo> mFileInfo; + int32_t mDelta; + int32_t mSavepointDelta; + + public: + explicit FileInfoEntry(SafeRefPtr<DatabaseFileInfo> aFileInfo) + : mFileInfo(std::move(aFileInfo)), mDelta(0), mSavepointDelta(0) { + MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry); + } + + void IncDeltas(bool aUpdateSavepointDelta) { + ++mDelta; + if (aUpdateSavepointDelta) { + ++mSavepointDelta; + } + } + void DecDeltas(bool aUpdateSavepointDelta) { + --mDelta; + if (aUpdateSavepointDelta) { + --mSavepointDelta; + } + } + void DecBySavepointDelta() { mDelta -= mSavepointDelta; } + SafeRefPtr<DatabaseFileInfo> ReleaseFileInfo() { + return std::move(mFileInfo); + } + void MaybeUpdateDBRefs() { + if (mDelta) { + mFileInfo->UpdateDBRefs(mDelta); + } + } + + int32_t Delta() const { return mDelta; } + int32_t SavepointDelta() const { return mSavepointDelta; } + + ~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; + + class ThreadRunnable; + class TransactionInfo; + struct TransactionInfoPair; + + struct IdleResource { + TimeStamp mIdleTime; + + IdleResource(const IdleResource& aOther) = delete; + IdleResource(IdleResource&& aOther) noexcept + : IdleResource(aOther.mIdleTime) {} + IdleResource& operator=(const IdleResource& aOther) = delete; + IdleResource& operator=(IdleResource&& aOther) = delete; + + protected: + explicit IdleResource(const TimeStamp& aIdleTime); + + ~IdleResource(); + }; + + struct IdleDatabaseInfo final : public IdleResource { + InitializedOnce<const NotNull<DatabaseInfo*>> mDatabaseInfo; + + public: + explicit IdleDatabaseInfo(DatabaseInfo& aDatabaseInfo); + + IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete; + IdleDatabaseInfo(IdleDatabaseInfo&& aOther) noexcept + : IdleResource(std::move(aOther)), + mDatabaseInfo{std::move(aOther.mDatabaseInfo)} { + MOZ_ASSERT(mDatabaseInfo); + + MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo); + } + IdleDatabaseInfo& operator=(const IdleDatabaseInfo& aOther) = delete; + IdleDatabaseInfo& operator=(IdleDatabaseInfo&& aOther) = delete; + + ~IdleDatabaseInfo(); + + bool operator==(const IdleDatabaseInfo& aOther) const { + return *mDatabaseInfo == *aOther.mDatabaseInfo; + } + + bool operator==(const DatabaseInfo* aDatabaseInfo) const { + return *mDatabaseInfo == aDatabaseInfo; + } + + bool operator<(const IdleDatabaseInfo& aOther) const { + return mIdleTime < aOther.mIdleTime; + } + }; + + class ThreadInfo { + public: + ThreadInfo(); + + ThreadInfo(nsCOMPtr<nsIThread> aThread, RefPtr<ThreadRunnable> aRunnable) + : mThread{std::move(aThread)}, mRunnable{std::move(aRunnable)} { + AssertIsOnBackgroundThread(); + AssertValid(); + + MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); + } + + ThreadInfo(const ThreadInfo& aOther) = delete; + ThreadInfo& operator=(const ThreadInfo& aOther) = delete; + + ThreadInfo(ThreadInfo&& aOther) noexcept; + ThreadInfo& operator=(ThreadInfo&& aOther) = default; + + bool IsValid() const { + const bool res = mThread; + if (res) { + AssertValid(); + } else { + AssertEmpty(); + } + return res; + } + + void AssertValid() const { + MOZ_ASSERT(mThread); + MOZ_ASSERT(mRunnable); + } + + void AssertEmpty() const { + MOZ_ASSERT(!mThread); + MOZ_ASSERT(!mRunnable); + } + + nsIThread& ThreadRef() { + AssertValid(); + return *mThread; + } + + std::tuple<nsCOMPtr<nsIThread>, RefPtr<ThreadRunnable>> Forget() { + AssertValid(); + + return {std::move(mThread), std::move(mRunnable)}; + } + + ~ThreadInfo(); + + bool operator==(const ThreadInfo& aOther) const { + return mThread == aOther.mThread && mRunnable == aOther.mRunnable; + } + + private: + nsCOMPtr<nsIThread> mThread; + RefPtr<ThreadRunnable> mRunnable; + }; + + struct IdleThreadInfo final : public IdleResource { + ThreadInfo mThreadInfo; + + explicit IdleThreadInfo(ThreadInfo aThreadInfo); + + IdleThreadInfo(const IdleThreadInfo& aOther) = delete; + IdleThreadInfo(IdleThreadInfo&& aOther) noexcept + : IdleResource(std::move(aOther)), + mThreadInfo(std::move(aOther.mThreadInfo)) { + AssertIsOnBackgroundThread(); + mThreadInfo.AssertValid(); + + MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo); + } + IdleThreadInfo& operator=(const IdleThreadInfo& aOther) = delete; + IdleThreadInfo& operator=(IdleThreadInfo&& aOther) = delete; + + ~IdleThreadInfo(); + + bool operator==(const IdleThreadInfo& aOther) const { + return mThreadInfo == aOther.mThreadInfo; + } + + bool operator<(const IdleThreadInfo& aOther) const { + return mIdleTime < aOther.mIdleTime; + } + }; + + // This mutex guards mDatabases, see below. + Mutex mDatabasesMutex MOZ_UNANNOTATED; + + nsTArray<IdleThreadInfo> mIdleThreads; + nsTArray<IdleDatabaseInfo> mIdleDatabases; + nsTArray<NotNull<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<NotNull<TransactionInfo*>> mQueuedTransactions; + + nsTArray<UniquePtr<DatabasesCompleteCallback>> mCompleteCallbacks; + + uint64_t mNextTransactionId; + uint32_t mTotalThreadCount; + FlippedOnce<false> mShutdownRequested; + FlippedOnce<false> mShutdownComplete; + + public: + ConnectionPool(); + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(ConnectionPool); + } + + Result<RefPtr<DatabaseConnection>, nsresult> GetOrCreateConnection( + const Database& aDatabase); + + 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) const; + + bool CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId); +}; + +class ConnectionPool::ConnectionRunnable : public Runnable { + protected: + DatabaseInfo& mDatabaseInfo; + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + + explicit ConnectionRunnable(DatabaseInfo& aDatabaseInfo); + + ~ConnectionRunnable() override = default; +}; + +class ConnectionPool::IdleConnectionRunnable final : public ConnectionRunnable { + const bool mNeedsCheckpoint; + + public: + IdleConnectionRunnable(DatabaseInfo& aDatabaseInfo, bool aNeedsCheckpoint) + : ConnectionRunnable(aDatabaseInfo), mNeedsCheckpoint(aNeedsCheckpoint) {} + + NS_INLINE_DECL_REFCOUNTING_INHERITED(IdleConnectionRunnable, + ConnectionRunnable) + + private: + ~IdleConnectionRunnable() override = default; + + NS_DECL_NSIRUNNABLE +}; + +class ConnectionPool::CloseConnectionRunnable final + : public ConnectionRunnable { + public: + explicit CloseConnectionRunnable(DatabaseInfo& aDatabaseInfo) + : ConnectionRunnable(aDatabaseInfo) {} + + NS_INLINE_DECL_REFCOUNTING_INHERITED(CloseConnectionRunnable, + ConnectionRunnable) + + private: + ~CloseConnectionRunnable() override = default; + + NS_DECL_NSIRUNNABLE +}; + +struct ConnectionPool::DatabaseInfo final { + friend class mozilla::DefaultDelete<DatabaseInfo>; + + RefPtr<ConnectionPool> mConnectionPool; + const nsCString mDatabaseId; + RefPtr<DatabaseConnection> mConnection; + nsClassHashtable<nsStringHashKey, TransactionInfoPair> mBlockingTransactions; + nsTArray<NotNull<TransactionInfo*>> mTransactionsScheduledDuringClose; + nsTArray<NotNull<TransactionInfo*>> mScheduledWriteTransactions; + Maybe<TransactionInfo&> mRunningWriteTransaction; + ThreadInfo mThreadInfo; + uint32_t mReadTransactionCount; + uint32_t mWriteTransactionCount; + bool mNeedsCheckpoint; + bool mIdle; + FlippedOnce<false> 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 DefaultDelete<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() = default; + + virtual ~FinishCallback() = default; +}; + +class ConnectionPool::FinishCallbackWrapper final : public Runnable { + RefPtr<ConnectionPool> mConnectionPool; + RefPtr<FinishCallback> mCallback; + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + uint64_t mTransactionId; + bool mHasRunOnce; + + public: + FinishCallbackWrapper(ConnectionPool* aConnectionPool, + uint64_t aTransactionId, FinishCallback* aCallback); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishCallbackWrapper, Runnable) + + private: + ~FinishCallbackWrapper() override; + + NS_DECL_NSIRUNNABLE +}; + +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. + FlippedOnce<true> mFirstRun; + FlippedOnce<true> mContinueRunning; + + public: + ThreadRunnable(); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ThreadRunnable, Runnable) + + uint32_t SerialNumber() const { return mSerialNumber; } + + nsCString GetThreadName() const { + return nsPrintfCString("IndexedDB #%" PRIu32, mSerialNumber); + } + + private: + ~ThreadRunnable() override; + + NS_DECL_NSIRUNNABLE +}; + +class ConnectionPool::TransactionInfo final { + friend class mozilla::DefaultDelete<TransactionInfo>; + + nsTHashSet<TransactionInfo*> mBlocking; + nsTArray<NotNull<TransactionInfo*>> mBlockingOrdered; + + public: + DatabaseInfo& mDatabaseInfo; + const nsID mBackgroundChildLoggingId; + const nsCString mDatabaseId; + const uint64_t mTransactionId; + const int64_t mLoggingSerialNumber; + const nsTArray<nsString> mObjectStoreNames; + nsTHashSet<TransactionInfo*> mBlockedOn; + nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables; + const bool mIsWriteTransaction; + bool mRunning; + +#ifdef DEBUG + FlippedOnce<false> 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 { + // Multiple reading transactions can block future writes. + nsTArray<NotNull<TransactionInfo*>> mLastBlockingWrites; + // But only a single writing transaction can block future reads. + Maybe<TransactionInfo&> mLastBlockingReads; + +#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) + TransactionInfoPair(); + ~TransactionInfoPair(); +#endif +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +template <IDBCursorType CursorType> +class CommonOpenOpHelper; +template <IDBCursorType CursorType> +class IndexOpenOpHelper; +template <IDBCursorType CursorType> +class ObjectStoreOpenOpHelper; +template <IDBCursorType CursorType> +class OpenOpHelper; + +class DatabaseOperationBase : public Runnable, + public mozIStorageProgressHandler { + template <IDBCursorType CursorType> + friend class OpenOpHelper; + + protected: + class AutoSetProgressHandler; + + using UniqueIndexTable = nsTHashMap<nsUint64HashKey, bool>; + + const nsCOMPtr<nsIEventTarget> mOwningEventTarget; + const nsID mBackgroundChildLoggingId; + const uint64_t mLoggingSerialNumber; + + private: + nsresult mResultCode = NS_OK; + Atomic<bool> mOperationMayProceed; + FlippedOnce<false> mActorDestroyed; + + public: + NS_DECL_ISUPPORTS_INHERITED + + bool IsOnOwningThread() const { + MOZ_ASSERT(mOwningEventTarget); + + bool current; + return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && + current; + } + + void AssertIsOnOwningThread() const { + MOZ_ASSERT(IsOnBackgroundThread()); + MOZ_ASSERT(IsOnOwningThread()); + } + + void NoteActorDestroyed() { + AssertIsOnOwningThread(); + + mActorDestroyed.EnsureFlipped(); + 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 aFailureCode) { + MOZ_ASSERT(NS_SUCCEEDED(mResultCode)); + OverrideFailureCode(aFailureCode); + } + + void SetFailureCodeIfUnset(nsresult aFailureCode) { + if (NS_SUCCEEDED(mResultCode)) { + OverrideFailureCode(aFailureCode); + } + } + + bool HasFailed() const { return NS_FAILED(mResultCode); } + + protected: + DatabaseOperationBase(const nsID& aBackgroundChildLoggingId, + uint64_t aLoggingSerialNumber) + : Runnable("dom::indexedDB::DatabaseOperationBase"), + mOwningEventTarget(GetCurrentSerialEventTarget()), + mBackgroundChildLoggingId(aBackgroundChildLoggingId), + mLoggingSerialNumber(aLoggingSerialNumber), + mOperationMayProceed(true) { + AssertIsOnOwningThread(); + } + + ~DatabaseOperationBase() override { MOZ_ASSERT(mActorDestroyed); } + + void OverrideFailureCode(nsresult aFailureCode) { + MOZ_ASSERT(NS_FAILED(aFailureCode)); + + mResultCode = aFailureCode; + } + + static nsAutoCString MaybeGetBindingClauseForKeyRange( + const Maybe<SerializedKeyRange>& aOptionalKeyRange, + const nsACString& aKeyColumnName); + + static nsAutoCString GetBindingClauseForKeyRange( + const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName); + + static uint64_t ReinterpretDoubleAsUInt64(double aDouble); + + static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, + mozIStorageStatement* aStatement); + + static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange, + mozIStorageStatement* aStatement, + const nsCString& aLocale); + + static Result<IndexDataValuesAutoArray, nsresult> + IndexDataValuesFromUpdateInfos(const nsTArray<IndexUpdateInfo>& aUpdateInfos, + const UniqueIndexTable& aUniqueIndexTable); + + static nsresult InsertIndexTableRows( + DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId, + const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues); + + static nsresult DeleteIndexDataTableRows( + DatabaseConnection* aConnection, const Key& aObjectStoreKey, + const nsTArray<IndexDataValue>& aIndexValues); + + static nsresult DeleteObjectStoreDataTableRowsWithIndexes( + DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId, + const Maybe<SerializedKeyRange>& aKeyRange); + + static nsresult UpdateIndexValues( + DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId, + const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues); + + static Result<bool, nsresult> ObjectStoreHasIndexes( + DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId); + + private: + template <typename KeyTransformation> + static nsresult MaybeBindKeyToStatement( + const Key& aKey, mozIStorageStatement* aStatement, + const nsACString& aParameterName, + const KeyTransformation& aKeyTransformation); + + template <typename KeyTransformation> + static nsresult BindTransformedKeyRangeToStatement( + const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement, + const KeyTransformation& aKeyTransformation); + + // Not to be overridden by subclasses. + NS_DECL_MOZISTORAGEPROGRESSHANDLER +}; + +class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final { + Maybe<mozIStorageConnection&> mConnection; +#ifdef DEBUG + DatabaseOperationBase* mDEBUGDatabaseOp; +#endif + + public: + AutoSetProgressHandler(); + + ~AutoSetProgressHandler(); + + nsresult Register(mozIStorageConnection& aConnection, + DatabaseOperationBase* aDatabaseOp); + + void Unregister(); +}; + +class TransactionDatabaseOperationBase : public DatabaseOperationBase { + enum class InternalState { + Initial, + DatabaseWork, + SendingPreprocess, + WaitingForContinue, + SendingResults, + Completed + }; + + InitializedOnce<const NotNull<SafeRefPtr<TransactionBase>>> mTransaction; + InternalState mInternalState = InternalState::Initial; + bool mWaitingForContinue = false; + const bool mTransactionIsAborted; + + protected: + const int64_t mTransactionLoggingSerialNumber; + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + protected: + // A check only enables when the diagnostic assert turns on. It assumes the + // mUpdateRefcountFunction is a nullptr because the previous + // StartTransactionOp failed on the connection thread and the next write + // operation (e.g. ObjectstoreAddOrPutRequestOp) doesn't have enough time to + // catch up the failure information. + bool mAssumingPreviousOperationFail = false; +#endif + + 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() { return **mTransaction; } + + const TransactionBase& Transaction() const { return **mTransaction; } + + bool IsWaitingForContinue() const { + AssertIsOnOwningThread(); + + return mWaitingForContinue; + } + + void NoteContinueReceived(); + + int64_t TransactionLoggingSerialNumber() const { + return mTransactionLoggingSerialNumber; + } + + // 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( + SafeRefPtr<TransactionBase> aTransaction); + + TransactionDatabaseOperationBase(SafeRefPtr<TransactionBase> aTransaction, + uint64_t aLoggingSerialNumber); + + ~TransactionDatabaseOperationBase() override; + + 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; + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + auto MakeAutoSavepointCleanupHandler(DatabaseConnection& aConnection) { + return [this, &aConnection](const auto) { + if (!aConnection.GetUpdateRefcountFunction()) { + mAssumingPreviousOperationFail = true; + } + }; + } +#endif + + 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, + public AtomicSafeRefCounted<Factory> { + RefPtr<DatabaseLoggingInfo> mLoggingInfo; + +#ifdef DEBUG + bool mActorDestroyed; +#endif + + // Reference counted. + ~Factory() override; + + public: + [[nodiscard]] static SafeRefPtr<Factory> Create( + const LoggingInfo& aLoggingInfo); + + DatabaseLoggingInfo* GetLoggingInfo() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mLoggingInfo); + + return mLoggingInfo; + } + + MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Factory) + MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(Factory, AtomicSafeRefCounted) + + // Only constructed in Create(). + explicit Factory(RefPtr<DatabaseLoggingInfo> aLoggingInfo); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + PBackgroundIDBFactoryRequestParent* AllocPBackgroundIDBFactoryRequestParent( + const FactoryRequestParams& aParams) override; + + mozilla::ipc::IPCResult RecvPBackgroundIDBFactoryRequestConstructor( + PBackgroundIDBFactoryRequestParent* aActor, + const FactoryRequestParams& aParams) override; + + bool DeallocPBackgroundIDBFactoryRequestParent( + PBackgroundIDBFactoryRequestParent* aActor) override; + + PBackgroundIDBDatabaseParent* AllocPBackgroundIDBDatabaseParent( + const DatabaseSpec& aSpec, + NotNull<PBackgroundIDBFactoryRequestParent*> aRequest) override; + + bool DeallocPBackgroundIDBDatabaseParent( + PBackgroundIDBDatabaseParent* aActor) override; +}; + +class WaitForTransactionsHelper final : public Runnable { + const nsCString mDatabaseId; + nsCOMPtr<nsIRunnable> mCallback; + + enum class State { Initial = 0, WaitingForTransactions, Complete } mState; + + public: + WaitForTransactionsHelper(const nsACString& aDatabaseId, + nsIRunnable* aCallback) + : Runnable("dom::indexedDB::WaitForTransactionsHelper"), + mDatabaseId(aDatabaseId), + mCallback(aCallback), + mState(State::Initial) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!aDatabaseId.IsEmpty()); + MOZ_ASSERT(aCallback); + } + + void WaitForTransactions(); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(WaitForTransactionsHelper, Runnable) + + private: + ~WaitForTransactionsHelper() override { + MOZ_ASSERT(!mCallback); + MOZ_ASSERT(mState == State::Complete); + } + + void MaybeWaitForTransactions(); + + void CallCallback(); + + NS_DECL_NSIRUNNABLE +}; + +class Database final + : public PBackgroundIDBDatabaseParent, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>>, + public AtomicSafeRefCounted<Database> { + friend class VersionChangeTransaction; + + class StartTransactionOp; + class UnmapBlobCallback; + + private: + SafeRefPtr<Factory> mFactory; + SafeRefPtr<FullDatabaseMetadata> mMetadata; + SafeRefPtr<DatabaseFileManager> mFileManager; + RefPtr<DirectoryLock> mDirectoryLock; + nsTHashSet<TransactionBase*> mTransactions; + nsTHashMap<nsIDHashKey, SafeRefPtr<DatabaseFileInfo>> mMappedBlobs; + RefPtr<DatabaseConnection> mConnection; + const PrincipalInfo mPrincipalInfo; + const Maybe<ContentParentId> mOptionalContentParentId; + // XXX Consider changing this to ClientMetadata. + const quota::OriginMetadata mOriginMetadata; + const nsCString mId; + const nsString mFilePath; + const Maybe<const CipherKey> mKey; + int64_t mDirectoryLockId; + const uint32_t mTelemetryId; + const PersistenceType mPersistenceType; + const bool mChromeWriteAccessAllowed; + const bool mInPrivateBrowsing; + FlippedOnce<false> mClosed; + FlippedOnce<false> mInvalidated; + FlippedOnce<false> mActorWasAlive; + FlippedOnce<false> mActorDestroyed; + nsCOMPtr<nsIEventTarget> mBackgroundThread; +#ifdef DEBUG + bool mAllBlobsUnmapped; +#endif + + public: + // Created by OpenDatabaseOp. + Database(SafeRefPtr<Factory> aFactory, const PrincipalInfo& aPrincipalInfo, + const Maybe<ContentParentId>& aOptionalContentParentId, + const quota::OriginMetadata& aOriginMetadata, uint32_t aTelemetryId, + SafeRefPtr<FullDatabaseMetadata> aMetadata, + SafeRefPtr<DatabaseFileManager> aFileManager, + RefPtr<DirectoryLock> aDirectoryLock, bool aChromeWriteAccessAllowed, + bool aInPrivateBrowsing, const Maybe<const CipherKey>& aMaybeKey); + + void AssertIsOnConnectionThread() const { +#ifdef DEBUG + if (mConnection) { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + } else { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mInvalidated); + } +#endif + } + + MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Database) + + void Invalidate(); + + bool IsOwnedByProcess(ContentParentId aContentParentId) const { + return mOptionalContentParentId && + mOptionalContentParentId.value() == aContentParentId; + } + + const quota::OriginMetadata& OriginMetadata() const { + return mOriginMetadata; + } + + const nsCString& Id() const { return mId; } + + Maybe<DirectoryLock&> MaybeDirectoryLockRef() const { + AssertIsOnBackgroundThread(); + + return ToMaybeRef(mDirectoryLock.get()); + } + + int64_t DirectoryLockId() const { return mDirectoryLockId; } + + uint32_t TelemetryId() const { return mTelemetryId; } + + PersistenceType Type() const { return mPersistenceType; } + + const nsString& FilePath() const { return mFilePath; } + + DatabaseFileManager& GetFileManager() const { return *mFileManager; } + + MovingNotNull<SafeRefPtr<DatabaseFileManager>> GetFileManagerPtr() const { + return WrapMovingNotNull(mFileManager.clonePtr()); + } + + const FullDatabaseMetadata& Metadata() const { + MOZ_ASSERT(mMetadata); + return *mMetadata; + } + + SafeRefPtr<FullDatabaseMetadata> MetadataPtr() const { + MOZ_ASSERT(mMetadata); + return mMetadata.clonePtr(); + } + + PBackgroundParent* GetBackgroundParent() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return Manager()->Manager(); + } + + DatabaseLoggingInfo* GetLoggingInfo() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFactory); + + return mFactory->GetLoggingInfo(); + } + + bool RegisterTransaction(TransactionBase& aTransaction); + + void UnregisterTransaction(TransactionBase& aTransaction); + + void SetActorAlive(); + + void MapBlob(const IPCBlob& aIPCBlob, SafeRefPtr<DatabaseFileInfo> aFileInfo); + + 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; + } + + void Stringify(nsACString& aResult) const; + + bool IsInPrivateBrowsing() const { + AssertIsOnBackgroundThread(); + return mInPrivateBrowsing; + } + + const Maybe<const CipherKey>& MaybeKeyRef() const { + // This can be called on any thread, as it is const. + MOZ_ASSERT(mKey.isSome() == mInPrivateBrowsing); + return mKey; + } + + ~Database() override { + MOZ_ASSERT(mClosed); + MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed); + + NS_ProxyRelease("ReleaseIDBFactory", mBackgroundThread.get(), + mFactory.forget()); + } + + private: + [[nodiscard]] SafeRefPtr<DatabaseFileInfo> GetBlob(const IPCBlob& aIPCBlob); + + void UnmapBlob(const nsID& aID); + + void UnmapAllBlobs(); + + bool CloseInternal(); + + void MaybeCloseConnection(); + + void ConnectionClosedCallback(); + + void CleanupMetadata(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + PBackgroundIDBDatabaseFileParent* AllocPBackgroundIDBDatabaseFileParent( + const IPCBlob& aIPCBlob) override; + + bool DeallocPBackgroundIDBDatabaseFileParent( + PBackgroundIDBDatabaseFileParent* aActor) override; + + already_AddRefed<PBackgroundIDBTransactionParent> + AllocPBackgroundIDBTransactionParent( + const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) override; + + mozilla::ipc::IPCResult RecvPBackgroundIDBTransactionConstructor( + PBackgroundIDBTransactionParent* aActor, + nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvBlocked() override; + + mozilla::ipc::IPCResult RecvClose() override; + + template <typename T> + static bool InvalidateAll(const nsTBaseHashSet<nsPtrHashKey<T>>& aTable); +}; + +class Database::StartTransactionOp final + : public TransactionDatabaseOperationBase { + friend class Database; + + private: + explicit StartTransactionOp(SafeRefPtr<TransactionBase> aTransaction) + : TransactionDatabaseOperationBase(std::move(aTransaction), + /* aLoggingSerialNumber */ 0) {} + + ~StartTransactionOp() override = default; + + void RunOnConnectionThread() override; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + nsresult SendSuccessResult() override; + + bool SendFailureResult(nsresult aResultCode) override; + + void Cleanup() override; +}; + +class Database::UnmapBlobCallback final + : public RemoteLazyInputStreamParentCallback { + SafeRefPtr<Database> mDatabase; + nsCOMPtr<nsISerialEventTarget> mBackgroundThread; + + public: + explicit UnmapBlobCallback(SafeRefPtr<Database> aDatabase) + : mDatabase(std::move(aDatabase)), + mBackgroundThread(GetCurrentSerialEventTarget()) { + AssertIsOnBackgroundThread(); + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Database::UnmapBlobCallback, override) + + void ActorDestroyed(const nsID& aID) override { + MOZ_ASSERT(mDatabase); + mBackgroundThread->Dispatch(NS_NewRunnableFunction( + "UnmapBlobCallback", [aID, database = std::move(mDatabase)] { + AssertIsOnBackgroundThread(); + database->UnmapBlob(aID); + })); + } + + private: + ~UnmapBlobCallback() = default; +}; + +/** + * 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 + * DatabaseFileInfo 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 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 done using the internal IPCBlob + * inputStream actor ID to DatabaseFileInfo mapping. 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 { + // 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. + InitializedOnce<const RefPtr<BlobImpl>> mBlobImpl; + const SafeRefPtr<DatabaseFileInfo> mFileInfo; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::DatabaseFile); + + const DatabaseFileInfo& GetFileInfo() const { + AssertIsOnBackgroundThread(); + + return *mFileInfo; + } + + SafeRefPtr<DatabaseFileInfo> GetFileInfoPtr() const { + AssertIsOnBackgroundThread(); + + return mFileInfo.clonePtr(); + } + + /** + * If mBlobImpl is non-null (implying the contents of this file have not yet + * been written to disk), then return an input stream. Otherwise, if mBlobImpl + * is null (because the contents have been written to disk), returns null. + */ + [[nodiscard]] nsCOMPtr<nsIInputStream> GetInputStream(ErrorResult& rv) const; + + /** + * To be called upon successful copying of the stream GetInputStream() + * returned so that we won't try and redundantly write the file to disk in the + * future. This is a separate step from GetInputStream() 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()); + + MOZ_ASSERT(*mBlobImpl); + mBlobImpl.destroy(); + } + + public: + // Called when sending to the child. + explicit DatabaseFile(SafeRefPtr<DatabaseFileInfo> aFileInfo) + : mBlobImpl{nullptr}, mFileInfo(std::move(aFileInfo)) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFileInfo); + } + + // Called when receiving from the child. + DatabaseFile(RefPtr<BlobImpl> aBlobImpl, + SafeRefPtr<DatabaseFileInfo> aFileInfo) + : mBlobImpl(std::move(aBlobImpl)), mFileInfo(std::move(aFileInfo)) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(*mBlobImpl); + MOZ_ASSERT(mFileInfo); + } + + private: + ~DatabaseFile() override = default; + + void ActorDestroy(ActorDestroyReason aWhy) override { + AssertIsOnBackgroundThread(); + } +}; + +nsCOMPtr<nsIInputStream> DatabaseFile::GetInputStream(ErrorResult& rv) const { + // We should only be called from our DB connection thread, not the background + // thread. + MOZ_ASSERT(!IsOnBackgroundThread()); + + // If we were constructed without a BlobImpl, or WriteSucceededClearBlobImpl + // was already called, return nullptr. + if (!mBlobImpl || !*mBlobImpl) { + return nullptr; + } + + nsCOMPtr<nsIInputStream> inputStream; + (*mBlobImpl)->CreateInputStream(getter_AddRefs(inputStream), rv); + if (rv.Failed()) { + return nullptr; + } + + return inputStream; +} + +class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { + friend class CursorBase; + + template <IDBCursorType CursorType> + friend class Cursor; + + class CommitOp; + + protected: + using Mode = IDBTransaction::Mode; + + private: + const SafeRefPtr<Database> mDatabase; + nsTArray<SafeRefPtr<FullObjectStoreMetadata>> + mModifiedAutoIncrementObjectStoreMetadataArray; + LazyInitializedOnceNotNull<const uint64_t> mTransactionId; + const nsCString mDatabaseId; + const int64_t mLoggingSerialNumber; + uint64_t mActiveRequestCount; + Atomic<bool> mInvalidatedOnAnyThread; + const Mode mMode; + FlippedOnce<false> mInitialized; + FlippedOnce<false> mHasBeenActiveOnConnectionThread; + FlippedOnce<false> mActorDestroyed; + FlippedOnce<false> mInvalidated; + + protected: + nsresult mResultCode; + FlippedOnce<false> mCommitOrAbortReceived; + FlippedOnce<false> mCommittedOrAborted; + FlippedOnce<false> mForceAborted; + LazyInitializedOnce<const Maybe<int64_t>> mLastRequestBeforeCommit; + Maybe<int64_t> mLastFailedRequest; + + 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 Init(const uint64_t aTransactionId) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTransactionId); + + mTransactionId.init(aTransactionId); + mInitialized.Flip(); + } + + void SetActiveOnConnectionThread() { + AssertIsOnConnectionThread(); + mHasBeenActiveOnConnectionThread.Flip(); + } + + MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::TransactionBase) + + void Abort(nsresult aResultCode, bool aForce); + + uint64_t TransactionId() const { return *mTransactionId; } + + const nsACString& DatabaseId() const { return mDatabaseId; } + + Mode GetMode() const { return mMode; } + + const Database& GetDatabase() const { + MOZ_ASSERT(mDatabase); + + return *mDatabase; + } + + Database& GetMutableDatabase() const { + MOZ_ASSERT(mDatabase); + + return *mDatabase; + } + + SafeRefPtr<Database> GetDatabasePtr() const { + MOZ_ASSERT(mDatabase); + + return mDatabase.clonePtr(); + } + + DatabaseLoggingInfo* GetLoggingInfo() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabase); + + return mDatabase->GetLoggingInfo(); + } + + int64_t LoggingSerialNumber() const { return mLoggingSerialNumber; } + + bool IsAborted() const { + AssertIsOnBackgroundThread(); + + return NS_FAILED(mResultCode); + } + + [[nodiscard]] SafeRefPtr<FullObjectStoreMetadata> GetMetadataForObjectStoreId( + IndexOrObjectStoreId aObjectStoreId) const; + + [[nodiscard]] SafeRefPtr<FullIndexMetadata> GetMetadataForIndexId( + FullObjectStoreMetadata& aObjectStoreMetadata, + IndexOrObjectStoreId aIndexId) const; + + PBackgroundParent* GetBackgroundParent() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + return GetDatabase().GetBackgroundParent(); + } + + void NoteModifiedAutoIncrementObjectStore( + const SafeRefPtr<FullObjectStoreMetadata>& aMetadata); + + void ForgetModifiedAutoIncrementObjectStore( + FullObjectStoreMetadata& aMetadata); + + void NoteActiveRequest(); + + void NoteFinishedRequest(int64_t aRequestId, nsresult aResultCode); + + void Invalidate(); + + virtual ~TransactionBase(); + + protected: + TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode); + + void NoteActorDestroyed() { + AssertIsOnBackgroundThread(); + + mActorDestroyed.Flip(); + } + +#ifdef DEBUG + // Only called by VersionChangeTransaction. + void FakeActorDestroyed() { mActorDestroyed.EnsureFlipped(); } +#endif + + mozilla::ipc::IPCResult RecvCommit(IProtocol* aActor, + const Maybe<int64_t> aLastRequest); + + mozilla::ipc::IPCResult RecvAbort(IProtocol* aActor, 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(RequestParams&& aParams, + bool aTrustParams); + + bool StartRequest(PBackgroundIDBRequestParent* aActor); + + bool DeallocRequest(PBackgroundIDBRequestParent* aActor); + + already_AddRefed<PBackgroundIDBCursorParent> AllocCursor( + const OpenCursorParams& aParams, bool aTrustParams); + + bool StartCursor(PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams); + + virtual void UpdateMetadata(nsresult aResult) {} + + virtual void SendCompleteNotification(nsresult aResult) = 0; + + private: + bool VerifyRequestParams(const RequestParams& aParams) const; + + bool VerifyRequestParams(const SerializedKeyRange& aParams) const; + + bool VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const; + + bool VerifyRequestParams(const Maybe<SerializedKeyRange>& aParams) const; + + void CommitOrAbort(); +}; + +class TransactionBase::CommitOp final : public DatabaseOperationBase, + public ConnectionPool::FinishCallback { + friend class TransactionBase; + + SafeRefPtr<TransactionBase> mTransaction; + nsresult mResultCode; ///< TODO: There is also a mResultCode in + ///< DatabaseOperationBase. Is there a reason not to + ///< use that? At least a more specific name should be + ///< given to this one. + + private: + CommitOp(SafeRefPtr<TransactionBase> aTransaction, nsresult aResultCode); + + ~CommitOp() override = default; + + // 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 + + void TransactionFinishedBeforeUnblock() override; + + void TransactionFinishedAfterUnblock() override; + + public: + // We need to declare all of nsISupports, because FinishCallback has + // a pure-virtual nsISupports declaration. + NS_DECL_ISUPPORTS_INHERITED +}; + +class NormalTransaction final : public TransactionBase, + public PBackgroundIDBTransactionParent { + nsTArray<SafeRefPtr<FullObjectStoreMetadata>> mObjectStores; + + // Reference counted. + ~NormalTransaction() override = default; + + bool IsSameProcessActor(); + + // Only called by TransactionBase. + void SendCompleteNotification(nsresult aResult) override; + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvCommit( + const Maybe<int64_t>& aLastRequest) override; + + mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override; + + PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent( + const RequestParams& aParams) override; + + mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor( + PBackgroundIDBRequestParent* aActor, + const RequestParams& aParams) override; + + bool DeallocPBackgroundIDBRequestParent( + PBackgroundIDBRequestParent* aActor) override; + + already_AddRefed<PBackgroundIDBCursorParent> AllocPBackgroundIDBCursorParent( + const OpenCursorParams& aParams) override; + + mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor( + PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) override; + + public: + // This constructor is only called by Database. + NormalTransaction( + SafeRefPtr<Database> aDatabase, TransactionBase::Mode aMode, + nsTArray<SafeRefPtr<FullObjectStoreMetadata>>&& aObjectStores); + + MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(NormalTransaction, TransactionBase) +}; + +class VersionChangeTransaction final + : public TransactionBase, + public PBackgroundIDBVersionChangeTransactionParent { + friend class OpenDatabaseOp; + + RefPtr<OpenDatabaseOp> mOpenDatabaseOp; + SafeRefPtr<FullDatabaseMetadata> mOldMetadata; + + FlippedOnce<false> mActorWasAlive; + + public: + // Only called by OpenDatabaseOp. + explicit VersionChangeTransaction(OpenDatabaseOp* aOpenDatabaseOp); + + MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(VersionChangeTransaction, + TransactionBase) + + private: + // Reference counted. + ~VersionChangeTransaction() override; + + bool IsSameProcessActor(); + + // Only called by OpenDatabaseOp. + bool CopyDatabaseMetadata(); + + void SetActorAlive(); + + // Only called by TransactionBase. + void UpdateMetadata(nsresult aResult) override; + + // Only called by TransactionBase. + void SendCompleteNotification(nsresult aResult) override; + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvCommit( + const Maybe<int64_t>& aLastRequest) override; + + mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override; + + mozilla::ipc::IPCResult RecvCreateObjectStore( + const ObjectStoreMetadata& aMetadata) override; + + mozilla::ipc::IPCResult RecvDeleteObjectStore( + const IndexOrObjectStoreId& aObjectStoreId) override; + + mozilla::ipc::IPCResult RecvRenameObjectStore( + const IndexOrObjectStoreId& aObjectStoreId, + const nsAString& aName) override; + + mozilla::ipc::IPCResult RecvCreateIndex( + const IndexOrObjectStoreId& aObjectStoreId, + const IndexMetadata& aMetadata) override; + + mozilla::ipc::IPCResult RecvDeleteIndex( + const IndexOrObjectStoreId& aObjectStoreId, + const IndexOrObjectStoreId& aIndexId) override; + + mozilla::ipc::IPCResult RecvRenameIndex( + const IndexOrObjectStoreId& aObjectStoreId, + const IndexOrObjectStoreId& aIndexId, const nsAString& aName) override; + + PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent( + const RequestParams& aParams) override; + + mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor( + PBackgroundIDBRequestParent* aActor, + const RequestParams& aParams) override; + + bool DeallocPBackgroundIDBRequestParent( + PBackgroundIDBRequestParent* aActor) override; + + already_AddRefed<PBackgroundIDBCursorParent> AllocPBackgroundIDBCursorParent( + const OpenCursorParams& aParams) override; + + mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor( + PBackgroundIDBCursorParent* aActor, + const OpenCursorParams& aParams) override; +}; + +class FactoryOp + : public DatabaseOperationBase, + public OpenDirectoryListener, + public PBackgroundIDBFactoryRequestParent, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + public: + struct MaybeBlockedDatabaseInfo final { + SafeRefPtr<Database> mDatabase; + bool mBlocked; + + MaybeBlockedDatabaseInfo(MaybeBlockedDatabaseInfo&&) = default; + MaybeBlockedDatabaseInfo& operator=(MaybeBlockedDatabaseInfo&&) = default; + + MOZ_IMPLICIT MaybeBlockedDatabaseInfo(SafeRefPtr<Database> aDatabase) + : mDatabase(std::move(aDatabase)), mBlocked(false) { + MOZ_ASSERT(mDatabase); + + MOZ_COUNT_CTOR(FactoryOp::MaybeBlockedDatabaseInfo); + } + + ~MaybeBlockedDatabaseInfo() { + MOZ_COUNT_DTOR(FactoryOp::MaybeBlockedDatabaseInfo); + } + + bool operator==(const Database* aOther) const { + return mDatabase == aOther; + } + + Database* operator->() const& MOZ_NO_ADDREF_RELEASE_ON_RETURN { + return mDatabase.unsafeGetRawPtr(); + } + }; + + 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, + + // Ensuring quota manager is created and opening directory on the + // PBackground thread. Next step is either SendingResults if quota manager + // is not available or DirectoryOpenPending if quota manager is available. + FinishOpen, + + // 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! + SafeRefPtr<Factory> mFactory; + + RefPtr<ThreadsafeContentParentHandle> mContentHandle; + + // Must be released on the main thread! + RefPtr<DirectoryLock> mDirectoryLock; + + RefPtr<FactoryOp> mDelayedOp; + nsTArray<MaybeBlockedDatabaseInfo> mMaybeBlockedDatabases; + + const CommonFactoryRequestParams mCommonParams; + OriginMetadata mOriginMetadata; + nsCString mDatabaseId; + nsString mDatabaseFilePath; + int64_t mDirectoryLockId; + State mState; + bool mWaitingForPermissionRetry; + bool mEnforcingQuota; + const bool mDeleting; + bool mChromeWriteAccessAllowed; + FlippedOnce<false> mInPrivateBrowsing; + + public: + const nsACString& Origin() const { + AssertIsOnOwningThread(); + + return mOriginMetadata.mOrigin; + } + + bool DatabaseFilePathIsKnown() const { + AssertIsOnOwningThread(); + + return !mDatabaseFilePath.IsEmpty(); + } + + const nsAString& DatabaseFilePath() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + + return mDatabaseFilePath; + } + + void NoteDatabaseBlocked(Database* aDatabase); + + void NoteDatabaseClosed(Database* aDatabase); + +#ifdef DEBUG + bool HasBlockedDatabases() const { return !mMaybeBlockedDatabases.IsEmpty(); } +#endif + + void StringifyState(nsACString& aResult) const; + + void Stringify(nsACString& aResult) const; + + protected: + FactoryOp(SafeRefPtr<Factory> aFactory, + RefPtr<ThreadsafeContentParentHandle> aContentHandle, + const CommonFactoryRequestParams& aCommonParams, bool aDeleting); + + ~FactoryOp() override { + // 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 DirectoryOpen(); + + nsresult SendToIOThread(); + + void WaitForTransactions(); + + void CleanupMetadata(); + + void FinishSendResults(); + + nsresult SendVersionChangeMessages(DatabaseActorInfo* aDatabaseActorInfo, + Maybe<Database&> aOpeningDatabase, + uint64_t aOldVersion, + const Maybe<uint64_t>& aNewVersion); + + // Methods that subclasses must implement. + virtual nsresult DatabaseOpen() = 0; + + virtual nsresult DoDatabaseWork() = 0; + + virtual nsresult BeginVersionChange() = 0; + + virtual bool AreActorsAlive() = 0; + + virtual nsresult DispatchToWorkThread() = 0; + + // Should only be called by Run(). + virtual void SendResults() = 0; + + // We need to declare refcounting unconditionally, because + // OpenDirectoryListener has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + // Common nsIRunnable implementation that subclasses may not override. + NS_IMETHOD + Run() final; + + // OpenDirectoryListener overrides. + void DirectoryLockAcquired(DirectoryLock* aLock) override; + + void DirectoryLockFailed() override; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual void SendBlockedNotification() = 0; + + private: + mozilla::Result<PermissionValue, nsresult> CheckPermission( + ContentParent* aContentParent); + + static bool CheckAtLeastOneAppHasPermission( + ContentParent* aContentParent, const nsACString& aPermissionString); + + nsresult FinishOpen(); + + // Test whether this FactoryOp needs to wait for the given op. + bool MustWaitFor(const FactoryOp& aExistingOp); +}; + +class OpenDatabaseOp final : public FactoryOp { + friend class Database; + friend class VersionChangeTransaction; + + class VersionChangeOp; + + SafeRefPtr<FullDatabaseMetadata> mMetadata; + + uint64_t mRequestedVersion; + SafeRefPtr<DatabaseFileManager> mFileManager; + + SafeRefPtr<Database> mDatabase; + SafeRefPtr<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(SafeRefPtr<Factory> aFactory, + RefPtr<ThreadsafeContentParentHandle> aContentHandle, + const CommonFactoryRequestParams& aParams); + + private: + ~OpenDatabaseOp() override { MOZ_ASSERT(!mVersionChangeOp); } + + nsresult LoadDatabaseInformation(mozIStorageConnection& aConnection); + + nsresult SendUpgradeNeeded(); + + void EnsureDatabaseActor(); + + nsresult EnsureDatabaseActorIsAlive(); + + mozilla::Result<DatabaseSpec, nsresult> MetadataToSpec() const; + + void AssertMetadataConsistency(const FullDatabaseMetadata& aMetadata) +#ifdef DEBUG + ; +#else + { + } +#endif + + void ConnectionClosedCallback(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsresult DatabaseOpen() override; + + nsresult DoDatabaseWork() override; + + nsresult BeginVersionChange() override; + + bool AreActorsAlive() override; + + void SendBlockedNotification() override; + + nsresult DispatchToWorkThread() override; + + void SendResults() override; + + static nsresult UpdateLocaleAwareIndex(mozIStorageConnection& aConnection, + const IndexMetadata& aIndexMetadata, + const nsCString& aLocale); +}; + +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.clonePtr(), + aOpenDatabaseOp->LoggingSerialNumber()), + mOpenDatabaseOp(aOpenDatabaseOp), + mRequestedVersion(aOpenDatabaseOp->mRequestedVersion), + mPreviousVersion( + aOpenDatabaseOp->mMetadata->mCommonMetadata.version()) { + MOZ_ASSERT(aOpenDatabaseOp); + MOZ_ASSERT(mRequestedVersion); + } + + ~VersionChangeOp() override { MOZ_ASSERT(!mOpenDatabaseOp); } + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + nsresult SendSuccessResult() override; + + bool SendFailureResult(nsresult aResultCode) override; + + void Cleanup() override; +}; + +class DeleteDatabaseOp final : public FactoryOp { + class VersionChangeOp; + + nsString mDatabaseDirectoryPath; + nsString mDatabaseFilenameBase; + uint64_t mPreviousVersion; + + public: + DeleteDatabaseOp(SafeRefPtr<Factory> aFactory, + RefPtr<ThreadsafeContentParentHandle> aContentHandle, + const CommonFactoryRequestParams& aParams) + : FactoryOp(std::move(aFactory), std::move(aContentHandle), aParams, + /* aDeleting */ true), + mPreviousVersion(0) {} + + private: + ~DeleteDatabaseOp() override = default; + + void LoadPreviousVersion(nsIFile& aDatabaseFile); + + nsresult DatabaseOpen() override; + + nsresult DoDatabaseWork() override; + + nsresult BeginVersionChange() override; + + bool AreActorsAlive() override; + + void SendBlockedNotification() override; + + nsresult DispatchToWorkThread() override; + + 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() override = default; + + nsresult RunOnIOThread(); + + void RunOnOwningThread(); + + NS_DECL_NSIRUNNABLE +}; + +class VersionChangeTransactionOp : public TransactionDatabaseOperationBase { + public: + void Cleanup() override; + + protected: + explicit VersionChangeTransactionOp( + SafeRefPtr<VersionChangeTransaction> aTransaction) + : TransactionDatabaseOperationBase(std::move(aTransaction)) {} + + ~VersionChangeTransactionOp() override = default; + + private: + nsresult SendSuccessResult() override; + + bool SendFailureResult(nsresult aResultCode) override; +}; + +class CreateObjectStoreOp final : public VersionChangeTransactionOp { + friend class VersionChangeTransaction; + + const ObjectStoreMetadata mMetadata; + + private: + // Only created by VersionChangeTransaction. + CreateObjectStoreOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + const ObjectStoreMetadata& aMetadata) + : VersionChangeTransactionOp(std::move(aTransaction)), + mMetadata(aMetadata) { + MOZ_ASSERT(aMetadata.id()); + } + + ~CreateObjectStoreOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class DeleteObjectStoreOp final : public VersionChangeTransactionOp { + friend class VersionChangeTransaction; + + const SafeRefPtr<FullObjectStoreMetadata> mMetadata; + const bool mIsLastObjectStore; + + private: + // Only created by VersionChangeTransaction. + DeleteObjectStoreOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + SafeRefPtr<FullObjectStoreMetadata> aMetadata, + const bool aIsLastObjectStore) + : VersionChangeTransactionOp(std::move(aTransaction)), + mMetadata(std::move(aMetadata)), + mIsLastObjectStore(aIsLastObjectStore) { + MOZ_ASSERT(mMetadata->mCommonMetadata.id()); + } + + ~DeleteObjectStoreOp() override = default; + + 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(SafeRefPtr<VersionChangeTransaction> aTransaction, + FullObjectStoreMetadata& aMetadata) + : VersionChangeTransactionOp(std::move(aTransaction)), + mId(aMetadata.mCommonMetadata.id()), + mNewName(aMetadata.mCommonMetadata.name()) { + MOZ_ASSERT(mId); + } + + ~RenameObjectStoreOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class CreateIndexOp final : public VersionChangeTransactionOp { + friend class VersionChangeTransaction; + + class UpdateIndexDataValuesFunction; + + const IndexMetadata mMetadata; + Maybe<UniqueIndexTable> mMaybeUniqueIndexTable; + const SafeRefPtr<DatabaseFileManager> mFileManager; + const nsCString mDatabaseId; + const IndexOrObjectStoreId mObjectStoreId; + + private: + // Only created by VersionChangeTransaction. + CreateIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + IndexOrObjectStoreId aObjectStoreId, + const IndexMetadata& aMetadata); + + ~CreateIndexOp() override = default; + + nsresult InsertDataFromObjectStore(DatabaseConnection* aConnection); + + nsresult InsertDataFromObjectStoreInternal( + DatabaseConnection* aConnection) const; + + bool Init(TransactionBase& aTransaction) override; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class CreateIndexOp::UpdateIndexDataValuesFunction final + : public mozIStorageFunction { + RefPtr<CreateIndexOp> mOp; + RefPtr<DatabaseConnection> mConnection; + const NotNull<SafeRefPtr<Database>> mDatabase; + + public: + UpdateIndexDataValuesFunction(CreateIndexOp* aOp, + DatabaseConnection* aConnection, + SafeRefPtr<Database> aDatabase) + : mOp(aOp), + mConnection(aConnection), + mDatabase(WrapNotNull(std::move(aDatabase))) { + MOZ_ASSERT(aOp); + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + } + + NS_DECL_ISUPPORTS + + private: + ~UpdateIndexDataValuesFunction() = default; + + NS_DECL_MOZISTORAGEFUNCTION +}; + +class DeleteIndexOp final : public VersionChangeTransactionOp { + friend class VersionChangeTransaction; + + const IndexOrObjectStoreId mObjectStoreId; + const IndexOrObjectStoreId mIndexId; + const bool mUnique; + const bool mIsLastIndex; + + private: + // Only created by VersionChangeTransaction. + DeleteIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + IndexOrObjectStoreId aObjectStoreId, + IndexOrObjectStoreId aIndexId, const bool aUnique, + const bool aIsLastIndex); + + ~DeleteIndexOp() override = default; + + nsresult RemoveReferencesToIndex( + DatabaseConnection* aConnection, const Key& aObjectDataKey, + nsTArray<IndexDataValue>& aIndexValues) const; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class RenameIndexOp final : public VersionChangeTransactionOp { + friend class VersionChangeTransaction; + + const IndexOrObjectStoreId mObjectStoreId; + const IndexOrObjectStoreId mIndexId; + const nsString mNewName; + + private: + // Only created by VersionChangeTransaction. + RenameIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + FullIndexMetadata& aMetadata, + IndexOrObjectStoreId aObjectStoreId) + : VersionChangeTransactionOp(std::move(aTransaction)), + mObjectStoreId(aObjectStoreId), + mIndexId(aMetadata.mCommonMetadata.id()), + mNewName(aMetadata.mCommonMetadata.name()) { + MOZ_ASSERT(mIndexId); + } + + ~RenameIndexOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +class NormalTransactionOp : public TransactionDatabaseOperationBase, + public PBackgroundIDBRequestParent { +#ifdef DEBUG + bool mResponseSent; +#endif + + public: + void Cleanup() override; + + protected: + explicit NormalTransactionOp(SafeRefPtr<TransactionBase> aTransaction) + : TransactionDatabaseOperationBase(std::move(aTransaction)) +#ifdef DEBUG + , + mResponseSent(false) +#endif + { + } + + ~NormalTransactionOp() override = default; + + // An overload of DatabaseOperationBase's function that can avoid doing extra + // work on non-versionchange transactions. + mozilla::Result<bool, nsresult> ObjectStoreHasIndexes( + DatabaseConnection& aConnection, IndexOrObjectStoreId aObjectStoreId, + bool aMayHaveIndexes); + + virtual mozilla::Result<PreprocessParams, nsresult> GetPreprocessParams(); + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(RequestResponse& aResponse, + size_t* aResponseSize) = 0; + + private: + nsresult SendPreprocessInfo() override; + + nsresult SendSuccessResult() override; + + bool SendFailureResult(nsresult aResultCode) override; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvContinue( + const PreprocessResponse& aResponse) final; +}; + +class ObjectStoreAddOrPutRequestOp final : public NormalTransactionOp { + friend class TransactionBase; + + using PersistenceType = mozilla::dom::quota::PersistenceType; + + class StoredFileInfo final { + InitializedOnce<const NotNull<SafeRefPtr<DatabaseFileInfo>>> mFileInfo; + // Either nothing, a file actor or a non-Blob-backed inputstream to write to + // disk. + using FileActorOrInputStream = + Variant<Nothing, RefPtr<DatabaseFile>, nsCOMPtr<nsIInputStream>>; + InitializedOnce<const FileActorOrInputStream> mFileActorOrInputStream; +#ifdef DEBUG + const StructuredCloneFileBase::FileType mType; +#endif + void EnsureCipherKey(); + void AssertInvariants() const; + + StoredFileInfo(SafeRefPtr<DatabaseFileInfo> aFileInfo, + RefPtr<DatabaseFile> aFileActor); + + StoredFileInfo(SafeRefPtr<DatabaseFileInfo> aFileInfo, + nsCOMPtr<nsIInputStream> aInputStream); + + public: +#if defined(NS_BUILD_REFCNT_LOGGING) + // Only for MOZ_COUNT_CTOR. + StoredFileInfo(StoredFileInfo&& aOther) + : mFileInfo{std::move(aOther.mFileInfo)}, + mFileActorOrInputStream{std::move(aOther.mFileActorOrInputStream)} +# ifdef DEBUG + , + mType{aOther.mType} +# endif + { + MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); + } +#else + StoredFileInfo(StoredFileInfo&&) = default; +#endif + + static StoredFileInfo CreateForBlob(SafeRefPtr<DatabaseFileInfo> aFileInfo, + RefPtr<DatabaseFile> aFileActor); + static StoredFileInfo CreateForStructuredClone( + SafeRefPtr<DatabaseFileInfo> aFileInfo, + nsCOMPtr<nsIInputStream> aInputStream); + +#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) + ~StoredFileInfo() { + AssertIsOnBackgroundThread(); + AssertInvariants(); + + MOZ_COUNT_DTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); + } +#endif + + bool IsValid() const { return static_cast<bool>(mFileInfo); } + + const DatabaseFileInfo& GetFileInfo() const { return **mFileInfo; } + + bool ShouldCompress() const; + + void NotifyWriteSucceeded() const; + + using InputStreamResult = + mozilla::Result<nsCOMPtr<nsIInputStream>, nsresult>; + InputStreamResult GetInputStream(); + + void Serialize(nsString& aText) const; + }; + 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. + SafeRefPtr<FullObjectStoreMetadata> mMetadata; + + nsTArray<StoredFileInfo> mStoredFileInfos; + + Key mResponse; + const OriginMetadata mOriginMetadata; + const PersistenceType mPersistenceType; + const bool mOverwrite; + bool mObjectStoreMayHaveIndexes; + bool mDataOverThreshold; + + private: + // Only created by TransactionBase. + ObjectStoreAddOrPutRequestOp(SafeRefPtr<TransactionBase> aTransaction, + RequestParams&& aParams); + + ~ObjectStoreAddOrPutRequestOp() override = default; + + nsresult RemoveOldIndexDataValues(DatabaseConnection* aConnection); + + bool Init(TransactionBase& aTransaction) override; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; + + void Cleanup() override; +}; + +void ObjectStoreAddOrPutRequestOp::StoredFileInfo::AssertInvariants() const { + // The only allowed types are eStructuredClone, eBlob and eMutableFile. + MOZ_ASSERT(StructuredCloneFileBase::eStructuredClone == mType || + StructuredCloneFileBase::eBlob == mType || + StructuredCloneFileBase::eMutableFile == mType); + + // mFileInfo and a file actor in mFileActorOrInputStream are present until + // the object is moved away, but an inputStream in mFileActorOrInputStream + // can be released early. + MOZ_ASSERT_IF(static_cast<bool>(mFileActorOrInputStream) && + mFileActorOrInputStream->is<RefPtr<DatabaseFile>>(), + static_cast<bool>(mFileInfo)); + + if (mFileInfo) { + // In a non-moved StoredFileInfo, one of the following is true: + // - This was an overflow structured clone (eStructuredClone) and + // storedFileInfo.mFileActorOrInputStream CAN be a non-nullptr input + // stream (but that might have been release by ReleaseInputStream). + MOZ_ASSERT_IF( + StructuredCloneFileBase::eStructuredClone == mType, + !mFileActorOrInputStream || + (mFileActorOrInputStream->is<nsCOMPtr<nsIInputStream>>() && + mFileActorOrInputStream->as<nsCOMPtr<nsIInputStream>>())); + + // - This is a reference to a Blob (eBlob) that may or may not have + // already been written to disk. storedFileInfo.mFileActorOrInputStream + // MUST be a non-null file actor, but its GetInputStream may return + // nullptr (so don't assert on that). + MOZ_ASSERT_IF(StructuredCloneFileBase::eBlob == mType, + mFileActorOrInputStream->is<RefPtr<DatabaseFile>>() && + mFileActorOrInputStream->as<RefPtr<DatabaseFile>>()); + + // - It's a mutable file (eMutableFile). No writing will be performed, + // and storedFileInfo.mFileActorOrInputStream is Nothing. + MOZ_ASSERT_IF(StructuredCloneFileBase::eMutableFile == mType, + mFileActorOrInputStream->is<Nothing>()); + } +} + +void ObjectStoreAddOrPutRequestOp::StoredFileInfo::EnsureCipherKey() { + const auto& fileInfo = GetFileInfo(); + const auto& fileManager = fileInfo.Manager(); + + // No need to generate cipher keys if we are not in PBM + if (!fileManager.IsInPrivateBrowsingMode()) { + return; + } + + nsCString keyId; + keyId.AppendInt(fileInfo.Id()); + + fileManager.MutableCipherKeyManagerRef().Ensure(keyId); +} + +ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo( + SafeRefPtr<DatabaseFileInfo> aFileInfo, RefPtr<DatabaseFile> aFileActor) + : mFileInfo{WrapNotNull(std::move(aFileInfo))}, + mFileActorOrInputStream{std::move(aFileActor)} +#ifdef DEBUG + , + mType{StructuredCloneFileBase::eBlob} +#endif +{ + AssertIsOnBackgroundThread(); + AssertInvariants(); + + EnsureCipherKey(); + MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); +} + +ObjectStoreAddOrPutRequestOp::StoredFileInfo::StoredFileInfo( + SafeRefPtr<DatabaseFileInfo> aFileInfo, + nsCOMPtr<nsIInputStream> aInputStream) + : mFileInfo{WrapNotNull(std::move(aFileInfo))}, + mFileActorOrInputStream{std::move(aInputStream)} +#ifdef DEBUG + , + mType{StructuredCloneFileBase::eStructuredClone} +#endif +{ + AssertIsOnBackgroundThread(); + AssertInvariants(); + + EnsureCipherKey(); + MOZ_COUNT_CTOR(ObjectStoreAddOrPutRequestOp::StoredFileInfo); +} + +ObjectStoreAddOrPutRequestOp::StoredFileInfo +ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForBlob( + SafeRefPtr<DatabaseFileInfo> aFileInfo, RefPtr<DatabaseFile> aFileActor) { + return {std::move(aFileInfo), std::move(aFileActor)}; +} + +ObjectStoreAddOrPutRequestOp::StoredFileInfo +ObjectStoreAddOrPutRequestOp::StoredFileInfo::CreateForStructuredClone( + SafeRefPtr<DatabaseFileInfo> aFileInfo, + nsCOMPtr<nsIInputStream> aInputStream) { + return {std::move(aFileInfo), std::move(aInputStream)}; +} + +bool ObjectStoreAddOrPutRequestOp::StoredFileInfo::ShouldCompress() const { + // Must not be called after moving. + MOZ_ASSERT(IsValid()); + + // Compression is only necessary for eStructuredClone, i.e. when + // mFileActorOrInputStream stored an input stream. However, this is only + // called after GetInputStream, when mFileActorOrInputStream has been + // cleared, which is only possible for this type. + const bool res = !mFileActorOrInputStream; + MOZ_ASSERT(res == (StructuredCloneFileBase::eStructuredClone == mType)); + return res; +} + +void ObjectStoreAddOrPutRequestOp::StoredFileInfo::NotifyWriteSucceeded() + const { + MOZ_ASSERT(IsValid()); + + // For eBlob, clear the blob implementation. + if (mFileActorOrInputStream && + mFileActorOrInputStream->is<RefPtr<DatabaseFile>>()) { + mFileActorOrInputStream->as<RefPtr<DatabaseFile>>() + ->WriteSucceededClearBlobImpl(); + } + + // For the other types, no action is necessary. +} + +ObjectStoreAddOrPutRequestOp::StoredFileInfo::InputStreamResult +ObjectStoreAddOrPutRequestOp::StoredFileInfo::GetInputStream() { + if (!mFileActorOrInputStream) { + MOZ_ASSERT(StructuredCloneFileBase::eStructuredClone == mType); + return nsCOMPtr<nsIInputStream>{}; + } + + // For the different cases, see also the comments in AssertInvariants. + return mFileActorOrInputStream->match( + [](const Nothing&) -> InputStreamResult { + return nsCOMPtr<nsIInputStream>{}; + }, + [](const RefPtr<DatabaseFile>& databaseActor) -> InputStreamResult { + ErrorResult rv; + auto inputStream = databaseActor->GetInputStream(rv); + if (NS_WARN_IF(rv.Failed())) { + return Err(rv.StealNSResult()); + } + + return inputStream; + }, + [this](const nsCOMPtr<nsIInputStream>& inputStream) -> InputStreamResult { + auto res = inputStream; + // destroy() clears the inputStream parameter, so we needed to make a + // copy before + mFileActorOrInputStream.destroy(); + AssertInvariants(); + return res; + }); +} + +void ObjectStoreAddOrPutRequestOp::StoredFileInfo::Serialize( + nsString& aText) const { + AssertInvariants(); + MOZ_ASSERT(IsValid()); + + const int64_t id = (*mFileInfo)->Id(); + + auto structuredCloneHandler = [&aText, id](const nsCOMPtr<nsIInputStream>&) { + // eStructuredClone + aText.Append('.'); + aText.AppendInt(id); + }; + + // If mFileActorOrInputStream was moved, we had an inputStream before. + if (!mFileActorOrInputStream) { + structuredCloneHandler(nullptr); + return; + } + + // This encoding is parsed in DeserializeStructuredCloneFile. + mFileActorOrInputStream->match( + [&aText, id](const Nothing&) { + // eMutableFile + aText.AppendInt(-id); + }, + [&aText, id](const RefPtr<DatabaseFile>&) { + // eBlob + aText.AppendInt(id); + }, + structuredCloneHandler); +} + +class ObjectStoreAddOrPutRequestOp::SCInputStream final + : public nsIInputStream { + const JSStructuredCloneData& mData; + JSStructuredCloneData::Iterator mIter; + + public: + explicit SCInputStream(const JSStructuredCloneData& aData) + : mData(aData), mIter(aData.Start()) {} + + private: + virtual ~SCInputStream() = default; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM +}; + +class ObjectStoreGetRequestOp final : public NormalTransactionOp { + friend class TransactionBase; + + const IndexOrObjectStoreId mObjectStoreId; + SafeRefPtr<Database> mDatabase; + const Maybe<SerializedKeyRange> mOptionalKeyRange; + AutoTArray<StructuredCloneReadInfoParent, 1> mResponse; + PBackgroundParent* mBackgroundParent; + uint32_t mPreprocessInfoCount; + const uint32_t mLimit; + const bool mGetAll; + + private: + // Only created by TransactionBase. + ObjectStoreGetRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams, bool aGetAll); + + ~ObjectStoreGetRequestOp() override = default; + + template <typename T> + mozilla::Result<T, nsresult> ConvertResponse( + StructuredCloneReadInfoParent&& aInfo); + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + bool HasPreprocessInfo() override; + + mozilla::Result<PreprocessParams, nsresult> GetPreprocessParams() override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; +}; + +class ObjectStoreGetKeyRequestOp final : public NormalTransactionOp { + friend class TransactionBase; + + const IndexOrObjectStoreId mObjectStoreId; + const Maybe<SerializedKeyRange> mOptionalKeyRange; + const uint32_t mLimit; + const bool mGetAll; + nsTArray<Key> mResponse; + + private: + // Only created by TransactionBase. + ObjectStoreGetKeyRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams, bool aGetAll); + + ~ObjectStoreGetKeyRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; +}; + +class ObjectStoreDeleteRequestOp final : public NormalTransactionOp { + friend class TransactionBase; + + const ObjectStoreDeleteParams mParams; + ObjectStoreDeleteResponse mResponse; + bool mObjectStoreMayHaveIndexes; + + private: + ObjectStoreDeleteRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const ObjectStoreDeleteParams& aParams); + + ~ObjectStoreDeleteRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { + aResponse = std::move(mResponse); + *aResponseSize = 0; + } +}; + +class ObjectStoreClearRequestOp final : public NormalTransactionOp { + friend class TransactionBase; + + const ObjectStoreClearParams mParams; + ObjectStoreClearResponse mResponse; + bool mObjectStoreMayHaveIndexes; + + private: + ObjectStoreClearRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const ObjectStoreClearParams& aParams); + + ~ObjectStoreClearRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { + aResponse = std::move(mResponse); + *aResponseSize = 0; + } +}; + +class ObjectStoreCountRequestOp final : public NormalTransactionOp { + friend class TransactionBase; + + const ObjectStoreCountParams mParams; + ObjectStoreCountResponse mResponse; + + private: + ObjectStoreCountRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const ObjectStoreCountParams& aParams) + : NormalTransactionOp(std::move(aTransaction)), mParams(aParams) {} + + ~ObjectStoreCountRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { + aResponse = std::move(mResponse); + *aResponseSize = sizeof(uint64_t); + } +}; + +class IndexRequestOpBase : public NormalTransactionOp { + protected: + const SafeRefPtr<FullIndexMetadata> mMetadata; + + protected: + IndexRequestOpBase(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams) + : NormalTransactionOp(std::move(aTransaction)), + mMetadata(IndexMetadataForParams(Transaction(), aParams)) {} + + ~IndexRequestOpBase() override = default; + + private: + static SafeRefPtr<FullIndexMetadata> IndexMetadataForParams( + const TransactionBase& aTransaction, const RequestParams& aParams); +}; + +class IndexGetRequestOp final : public IndexRequestOpBase { + friend class TransactionBase; + + SafeRefPtr<Database> mDatabase; + const Maybe<SerializedKeyRange> mOptionalKeyRange; + AutoTArray<StructuredCloneReadInfoParent, 1> mResponse; + PBackgroundParent* mBackgroundParent; + const uint32_t mLimit; + const bool mGetAll; + + private: + // Only created by TransactionBase. + IndexGetRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams, bool aGetAll); + + ~IndexGetRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; +}; + +class IndexGetKeyRequestOp final : public IndexRequestOpBase { + friend class TransactionBase; + + const Maybe<SerializedKeyRange> mOptionalKeyRange; + AutoTArray<Key, 1> mResponse; + const uint32_t mLimit; + const bool mGetAll; + + private: + // Only created by TransactionBase. + IndexGetKeyRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams, bool aGetAll); + + ~IndexGetKeyRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override; +}; + +class IndexCountRequestOp final : public IndexRequestOpBase { + friend class TransactionBase; + + const IndexCountParams mParams; + IndexCountResponse mResponse; + + private: + // Only created by TransactionBase. + IndexCountRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams) + : IndexRequestOpBase(std::move(aTransaction), aParams), + mParams(aParams.get_IndexCountParams()) {} + + ~IndexCountRequestOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + void GetResponse(RequestResponse& aResponse, size_t* aResponseSize) override { + aResponse = std::move(mResponse); + *aResponseSize = sizeof(uint64_t); + } +}; + +template <IDBCursorType CursorType> +class Cursor; + +constexpr IDBCursorType ToKeyOnlyType(const IDBCursorType aType) { + MOZ_ASSERT(aType == IDBCursorType::ObjectStore || + aType == IDBCursorType::ObjectStoreKey || + aType == IDBCursorType::Index || aType == IDBCursorType::IndexKey); + switch (aType) { + case IDBCursorType::ObjectStore: + case IDBCursorType::ObjectStoreKey: + return IDBCursorType::ObjectStoreKey; + case IDBCursorType::Index: + case IDBCursorType::IndexKey: + return IDBCursorType::IndexKey; + } +} + +template <IDBCursorType CursorType> +using CursorPosition = CursorData<ToKeyOnlyType(CursorType)>; + +#ifdef DEBUG +constexpr indexedDB::OpenCursorParams::Type ToOpenCursorParamsType( + const IDBCursorType aType) { + MOZ_ASSERT(aType == IDBCursorType::ObjectStore || + aType == IDBCursorType::ObjectStoreKey || + aType == IDBCursorType::Index || aType == IDBCursorType::IndexKey); + switch (aType) { + case IDBCursorType::ObjectStore: + return indexedDB::OpenCursorParams::TObjectStoreOpenCursorParams; + case IDBCursorType::ObjectStoreKey: + return indexedDB::OpenCursorParams::TObjectStoreOpenKeyCursorParams; + case IDBCursorType::Index: + return indexedDB::OpenCursorParams::TIndexOpenCursorParams; + case IDBCursorType::IndexKey: + return indexedDB::OpenCursorParams::TIndexOpenKeyCursorParams; + } +} +#endif + +class CursorBase : public PBackgroundIDBCursorParent { + friend class TransactionBase; + template <IDBCursorType CursorType> + friend class CommonOpenOpHelper; + + protected: + const SafeRefPtr<TransactionBase> mTransaction; + + // This should only be touched on the PBackground thread to check whether + // the objectStore has been deleted. Holding these saves a hash lookup for + // every call to continue()/advance(). + InitializedOnce<const NotNull<SafeRefPtr<FullObjectStoreMetadata>>> + mObjectStoreMetadata; + + const IndexOrObjectStoreId mObjectStoreId; + + LazyInitializedOnce<const Key> + mLocaleAwareRangeBound; ///< If the cursor is based on a key range, the + ///< bound in the direction of iteration (e.g. + ///< the upper bound in case of mDirection == + ///< NEXT). If the cursor is based on a key, it + ///< is unset. If mLocale is set, this was + ///< converted to mLocale. + + const Direction mDirection; + + const int32_t mMaxExtraCount; + + const bool mIsSameProcessActor; + + struct ConstructFromTransactionBase {}; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::CursorBase, + final) + + CursorBase(SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<FullObjectStoreMetadata> aObjectStoreMetadata, + Direction aDirection, + ConstructFromTransactionBase aConstructionTag); + + protected: + // Reference counted. + ~CursorBase() override { MOZ_ASSERT(!mObjectStoreMetadata); } + + private: + virtual bool Start(const OpenCursorParams& aParams) = 0; +}; + +class IndexCursorBase : public CursorBase { + public: + bool IsLocaleAware() const { return !mLocale.IsEmpty(); } + + IndexCursorBase(SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<FullObjectStoreMetadata> aObjectStoreMetadata, + SafeRefPtr<FullIndexMetadata> aIndexMetadata, + Direction aDirection, + ConstructFromTransactionBase aConstructionTag) + : CursorBase{std::move(aTransaction), std::move(aObjectStoreMetadata), + aDirection, aConstructionTag}, + mIndexMetadata(WrapNotNull(std::move(aIndexMetadata))), + mIndexId((*mIndexMetadata)->mCommonMetadata.id()), + mUniqueIndex((*mIndexMetadata)->mCommonMetadata.unique()), + mLocale((*mIndexMetadata)->mCommonMetadata.locale()) {} + + protected: + IndexOrObjectStoreId Id() const { return mIndexId; } + + // This should only be touched on the PBackground thread to check whether + // the index has been deleted. Holding these saves a hash lookup for every + // call to continue()/advance(). + InitializedOnce<const NotNull<SafeRefPtr<FullIndexMetadata>>> mIndexMetadata; + const IndexOrObjectStoreId mIndexId; + const bool mUniqueIndex; + const nsCString + mLocale; ///< The locale if the cursor is locale-aware, otherwise empty. + + struct ContinueQueries { + nsCString mContinueQuery; + nsCString mContinueToQuery; + nsCString mContinuePrimaryKeyQuery; + + const nsACString& GetContinueQuery(const bool hasContinueKey, + const bool hasContinuePrimaryKey) const { + return hasContinuePrimaryKey ? mContinuePrimaryKeyQuery + : hasContinueKey ? mContinueToQuery + : mContinueQuery; + } + }; +}; + +class ObjectStoreCursorBase : public CursorBase { + public: + using CursorBase::CursorBase; + + static constexpr bool IsLocaleAware() { return false; } + + protected: + IndexOrObjectStoreId Id() const { return mObjectStoreId; } + + struct ContinueQueries { + nsCString mContinueQuery; + nsCString mContinueToQuery; + + const nsACString& GetContinueQuery(const bool hasContinueKey, + const bool hasContinuePrimaryKey) const { + MOZ_ASSERT(!hasContinuePrimaryKey); + return hasContinueKey ? mContinueToQuery : mContinueQuery; + } + }; +}; + +using FilesArray = nsTArray<nsTArray<StructuredCloneFileParent>>; + +struct PseudoFilesArray { + static constexpr bool IsEmpty() { return true; } + + static constexpr void Clear() {} +}; + +template <IDBCursorType CursorType> +using FilesArrayT = + std::conditional_t<!CursorTypeTraits<CursorType>::IsKeyOnlyCursor, + FilesArray, PseudoFilesArray>; + +class ValueCursorBase { + friend struct ValuePopulateResponseHelper<true>; + friend struct ValuePopulateResponseHelper<false>; + + protected: + explicit ValueCursorBase(TransactionBase* const aTransaction) + : mDatabase(aTransaction->GetDatabasePtr()), + mFileManager(mDatabase->GetFileManagerPtr()), + mBackgroundParent(WrapNotNull(aTransaction->GetBackgroundParent())) { + MOZ_ASSERT(mDatabase); + } + + void ProcessFiles(CursorResponse& aResponse, const FilesArray& aFiles); + + ~ValueCursorBase() { MOZ_ASSERT(!mBackgroundParent); } + + const SafeRefPtr<Database> mDatabase; + const NotNull<SafeRefPtr<DatabaseFileManager>> mFileManager; + + InitializedOnce<const NotNull<PBackgroundParent*>> mBackgroundParent; +}; + +class KeyCursorBase { + protected: + explicit KeyCursorBase(TransactionBase* const /*aTransaction*/) {} + + static constexpr void ProcessFiles(CursorResponse& aResponse, + const PseudoFilesArray& aFiles) {} +}; + +template <IDBCursorType CursorType> +class CursorOpBaseHelperBase; + +template <IDBCursorType CursorType> +class Cursor final + : public std::conditional_t< + CursorTypeTraits<CursorType>::IsObjectStoreCursor, + ObjectStoreCursorBase, IndexCursorBase>, + public std::conditional_t<CursorTypeTraits<CursorType>::IsKeyOnlyCursor, + KeyCursorBase, ValueCursorBase> { + using Base = + std::conditional_t<CursorTypeTraits<CursorType>::IsObjectStoreCursor, + ObjectStoreCursorBase, IndexCursorBase>; + + using KeyValueBase = + std::conditional_t<CursorTypeTraits<CursorType>::IsKeyOnlyCursor, + KeyCursorBase, ValueCursorBase>; + + static constexpr bool IsIndexCursor = + !CursorTypeTraits<CursorType>::IsObjectStoreCursor; + + static constexpr bool IsValueCursor = + !CursorTypeTraits<CursorType>::IsKeyOnlyCursor; + + class CursorOpBase; + class OpenOp; + class ContinueOp; + + using Base::Id; + using CursorBase::Manager; + using CursorBase::mDirection; + using CursorBase::mObjectStoreId; + using CursorBase::mTransaction; + using typename CursorBase::ActorDestroyReason; + + using TypedOpenOpHelper = + std::conditional_t<IsIndexCursor, IndexOpenOpHelper<CursorType>, + ObjectStoreOpenOpHelper<CursorType>>; + + friend class CursorOpBaseHelperBase<CursorType>; + friend class CommonOpenOpHelper<CursorType>; + friend TypedOpenOpHelper; + friend class OpenOpHelper<CursorType>; + + CursorOpBase* mCurrentlyRunningOp = nullptr; + + LazyInitializedOnce<const typename Base::ContinueQueries> mContinueQueries; + + // Only called by TransactionBase. + bool Start(const OpenCursorParams& aParams) final; + + void SendResponseInternal(CursorResponse& aResponse, + const FilesArrayT<CursorType>& aFiles); + + // Must call SendResponseInternal! + bool SendResponse(const CursorResponse& aResponse) = delete; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvContinue( + const CursorRequestParams& aParams, const Key& aCurrentKey, + const Key& aCurrentObjectStoreKey) override; + + public: + Cursor(SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<FullObjectStoreMetadata> aObjectStoreMetadata, + SafeRefPtr<FullIndexMetadata> aIndexMetadata, + typename Base::Direction aDirection, + typename Base::ConstructFromTransactionBase aConstructionTag) + : Base{std::move(aTransaction), std::move(aObjectStoreMetadata), + std::move(aIndexMetadata), aDirection, aConstructionTag}, + KeyValueBase{this->mTransaction.unsafeGetRawPtr()} {} + + Cursor(SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<FullObjectStoreMetadata> aObjectStoreMetadata, + typename Base::Direction aDirection, + typename Base::ConstructFromTransactionBase aConstructionTag) + : Base{std::move(aTransaction), std::move(aObjectStoreMetadata), + aDirection, aConstructionTag}, + KeyValueBase{this->mTransaction.unsafeGetRawPtr()} {} + + private: + void SetOptionalKeyRange(const Maybe<SerializedKeyRange>& aOptionalKeyRange, + bool* aOpen); + + bool VerifyRequestParams(const CursorRequestParams& aParams, + const CursorPosition<CursorType>& aPosition) const; + + ~Cursor() final = default; +}; + +template <IDBCursorType CursorType> +class Cursor<CursorType>::CursorOpBase + : public TransactionDatabaseOperationBase { + friend class CursorOpBaseHelperBase<CursorType>; + + protected: + RefPtr<Cursor> mCursor; + FilesArrayT<CursorType> mFiles; // TODO: Consider removing this member + // entirely if we are no value cursor. + + CursorResponse mResponse; + +#ifdef DEBUG + bool mResponseSent; +#endif + + protected: + explicit CursorOpBase(Cursor* aCursor) + : TransactionDatabaseOperationBase(aCursor->mTransaction.clonePtr()), + mCursor(aCursor) +#ifdef DEBUG + , + mResponseSent(false) +#endif + { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aCursor); + } + + ~CursorOpBase() override = default; + + bool SendFailureResult(nsresult aResultCode) final; + nsresult SendSuccessResult() final; + + void Cleanup() override; +}; + +template <IDBCursorType CursorType> +class OpenOpHelper; + +using ResponseSizeOrError = Result<size_t, nsresult>; + +template <IDBCursorType CursorType> +class CursorOpBaseHelperBase { + public: + explicit CursorOpBaseHelperBase( + typename Cursor<CursorType>::CursorOpBase& aOp) + : mOp{aOp} {} + + ResponseSizeOrError PopulateResponseFromStatement(mozIStorageStatement* aStmt, + bool aInitializeResponse, + Key* const aOptOutSortKey); + + void PopulateExtraResponses(mozIStorageStatement* aStmt, + uint32_t aMaxExtraCount, + const size_t aInitialResponseSize, + const nsACString& aOperation, + Key* const aOptPreviousSortKey); + + protected: + Cursor<CursorType>& GetCursor() { + MOZ_ASSERT(mOp.mCursor); + return *mOp.mCursor; + } + + void SetResponse(CursorResponse aResponse) { + mOp.mResponse = std::move(aResponse); + } + + protected: + typename Cursor<CursorType>::CursorOpBase& mOp; +}; + +class CommonOpenOpHelperBase { + protected: + static void AppendConditionClause(const nsACString& aColumnName, + const nsACString& aStatementParameterName, + bool aLessThan, bool aEquals, + nsCString& aResult); +}; + +template <IDBCursorType CursorType> +class CommonOpenOpHelper : public CursorOpBaseHelperBase<CursorType>, + protected CommonOpenOpHelperBase { + public: + explicit CommonOpenOpHelper(typename Cursor<CursorType>::OpenOp& aOp) + : CursorOpBaseHelperBase<CursorType>{aOp} {} + + protected: + using CursorOpBaseHelperBase<CursorType>::GetCursor; + using CursorOpBaseHelperBase<CursorType>::PopulateExtraResponses; + using CursorOpBaseHelperBase<CursorType>::PopulateResponseFromStatement; + using CursorOpBaseHelperBase<CursorType>::SetResponse; + + const Maybe<SerializedKeyRange>& GetOptionalKeyRange() const { + // This downcast is safe, since we initialized mOp from an OpenOp in the + // ctor. + return static_cast<typename Cursor<CursorType>::OpenOp&>(this->mOp) + .mOptionalKeyRange; + } + + nsresult ProcessStatementSteps(mozIStorageStatement* aStmt); +}; + +template <IDBCursorType CursorType> +class ObjectStoreOpenOpHelper : protected CommonOpenOpHelper<CursorType> { + public: + using CommonOpenOpHelper<CursorType>::CommonOpenOpHelper; + + protected: + using CommonOpenOpHelper<CursorType>::GetCursor; + using CommonOpenOpHelper<CursorType>::GetOptionalKeyRange; + using CommonOpenOpHelper<CursorType>::AppendConditionClause; + + void PrepareKeyConditionClauses(const nsACString& aDirectionClause, + const nsACString& aQueryStart); +}; + +template <IDBCursorType CursorType> +class IndexOpenOpHelper : protected CommonOpenOpHelper<CursorType> { + public: + using CommonOpenOpHelper<CursorType>::CommonOpenOpHelper; + + protected: + using CommonOpenOpHelper<CursorType>::GetCursor; + using CommonOpenOpHelper<CursorType>::GetOptionalKeyRange; + using CommonOpenOpHelper<CursorType>::AppendConditionClause; + + void PrepareIndexKeyConditionClause( + const nsACString& aDirectionClause, + const nsLiteralCString& aObjectDataKeyPrefix, nsAutoCString aQueryStart); +}; + +template <> +class OpenOpHelper<IDBCursorType::ObjectStore> + : public ObjectStoreOpenOpHelper<IDBCursorType::ObjectStore> { + public: + using ObjectStoreOpenOpHelper< + IDBCursorType::ObjectStore>::ObjectStoreOpenOpHelper; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection); +}; + +template <> +class OpenOpHelper<IDBCursorType::ObjectStoreKey> + : public ObjectStoreOpenOpHelper<IDBCursorType::ObjectStoreKey> { + public: + using ObjectStoreOpenOpHelper< + IDBCursorType::ObjectStoreKey>::ObjectStoreOpenOpHelper; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection); +}; + +template <> +class OpenOpHelper<IDBCursorType::Index> + : IndexOpenOpHelper<IDBCursorType::Index> { + private: + void PrepareKeyConditionClauses(const nsACString& aDirectionClause, + nsAutoCString aQueryStart) { + PrepareIndexKeyConditionClause(aDirectionClause, "index_table."_ns, + std::move(aQueryStart)); + } + + public: + using IndexOpenOpHelper<IDBCursorType::Index>::IndexOpenOpHelper; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection); +}; + +template <> +class OpenOpHelper<IDBCursorType::IndexKey> + : IndexOpenOpHelper<IDBCursorType::IndexKey> { + private: + void PrepareKeyConditionClauses(const nsACString& aDirectionClause, + nsAutoCString aQueryStart) { + PrepareIndexKeyConditionClause(aDirectionClause, ""_ns, + std::move(aQueryStart)); + } + + public: + using IndexOpenOpHelper<IDBCursorType::IndexKey>::IndexOpenOpHelper; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection); +}; + +template <IDBCursorType CursorType> +class Cursor<CursorType>::OpenOp final : public CursorOpBase { + friend class Cursor<CursorType>; + friend class CommonOpenOpHelper<CursorType>; + + const Maybe<SerializedKeyRange> mOptionalKeyRange; + + using CursorOpBase::mCursor; + using CursorOpBase::mResponse; + + // Only created by Cursor. + OpenOp(Cursor* const aCursor, + const Maybe<SerializedKeyRange>& aOptionalKeyRange) + : CursorOpBase(aCursor), mOptionalKeyRange(aOptionalKeyRange) {} + + // Reference counted. + ~OpenOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; +}; + +template <IDBCursorType CursorType> +class Cursor<CursorType>::ContinueOp final + : public Cursor<CursorType>::CursorOpBase { + friend class Cursor<CursorType>; + + using CursorOpBase::mCursor; + using CursorOpBase::mResponse; + const CursorRequestParams mParams; + + // Only created by Cursor. + ContinueOp(Cursor* const aCursor, CursorRequestParams aParams, + CursorPosition<CursorType> aPosition) + : CursorOpBase(aCursor), + mParams(std::move(aParams)), + mCurrentPosition{std::move(aPosition)} { + MOZ_ASSERT(mParams.type() != CursorRequestParams::T__None); + } + + // Reference counted. + ~ContinueOp() override = default; + + nsresult DoDatabaseWork(DatabaseConnection* aConnection) override; + + const CursorPosition<CursorType> mCurrentPosition; +}; + +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() override; + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvGetFileReferences( + const PersistenceType& aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt, + int32_t* aDBRefCnt, bool* aResult) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +struct DatabaseActorInfo final { + friend class mozilla::DefaultDelete<DatabaseActorInfo>; + + SafeRefPtr<FullDatabaseMetadata> mMetadata; + nsTArray<NotNull<CheckedUnsafePtr<Database>>> mLiveDatabases; + RefPtr<FactoryOp> mWaitingFactoryOp; + + DatabaseActorInfo(SafeRefPtr<FullDatabaseMetadata> aMetadata, + NotNull<Database*> aDatabase) + : mMetadata(std::move(aMetadata)) { + 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::Mode::VersionChange) { + 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 QuotaClient final : public mozilla::dom::quota::Client { + static QuotaClient* sInstance; + + nsCOMPtr<nsIEventTarget> mBackgroundThread; + nsCOMPtr<nsITimer> mDeleteTimer; + nsTArray<RefPtr<Maintenance>> mMaintenanceQueue; + RefPtr<Maintenance> mCurrentMaintenance; + RefPtr<nsThreadPool> mMaintenanceThreadPool; + nsClassHashtable<nsRefPtrHashKey<DatabaseFileManager>, nsTArray<int64_t>> + mPendingDeleteInfos; + + public: + QuotaClient(); + + static QuotaClient* GetInstance() { + AssertIsOnBackgroundThread(); + + return sInstance; + } + + nsIEventTarget* BackgroundThread() const { + MOZ_ASSERT(mBackgroundThread); + return mBackgroundThread; + } + + nsresult AsyncDeleteFile(DatabaseFileManager* aFileManager, int64_t aFileId); + + nsresult FlushPendingFileDeletions(); + + RefPtr<Maintenance> GetCurrentMaintenance() const { + return mCurrentMaintenance; + } + + void NoteFinishedMaintenance(Maintenance* aMaintenance) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aMaintenance); + MOZ_ASSERT(mCurrentMaintenance == aMaintenance); + + mCurrentMaintenance = nullptr; + + QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::IDB, + "Maintenance finished"_ns); + + ProcessMaintenanceQueue(); + } + + nsThreadPool* GetOrCreateThreadPool(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::QuotaClient, + override) + + mozilla::dom::quota::Client::Type GetType() override; + + nsresult UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) override; + + nsresult UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) override; + + Result<UsageInfo, nsresult> InitOrigin(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + Result<UsageInfo, nsresult> GetUsageForOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + void OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) override; + + void OnRepositoryClearCompleted(PersistenceType aPersistenceType) override; + + void ReleaseIOThreadObjects() override; + + void AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) override; + + void AbortOperationsForProcess(ContentParentId aContentParentId) override; + + void AbortAllOperations() override; + + void StartIdleMaintenance() override; + + void StopIdleMaintenance() override; + + private: + ~QuotaClient() override; + + void InitiateShutdown() override; + bool IsShutdownCompleted() const override; + nsCString GetShutdownStatus() const override; + void ForceKillActors() override; + void FinalizeShutdown() override; + + static void DeleteTimerCallback(nsITimer* aTimer, void* aClosure); + + Result<nsCOMPtr<nsIFile>, nsresult> GetDirectory( + const OriginMetadata& aOriginMetadata); + + struct SubdirectoriesToProcessAndDatabaseFilenames { + AutoTArray<nsString, 20> subdirsToProcess; + nsTHashSet<nsString> databaseFilenames{20}; + }; + + struct SubdirectoriesToProcessAndDatabaseFilenamesAndObsoleteFilenames { + AutoTArray<nsString, 20> subdirsToProcess; + nsTHashSet<nsString> databaseFilenames{20}; + nsTHashSet<nsString> obsoleteFilenames{20}; + }; + + enum class ObsoleteFilenamesHandling { Include, Omit }; + + template <ObsoleteFilenamesHandling ObsoleteFilenames> + using GetDatabaseFilenamesResult = std::conditional_t< + ObsoleteFilenames == ObsoleteFilenamesHandling::Include, + SubdirectoriesToProcessAndDatabaseFilenamesAndObsoleteFilenames, + SubdirectoriesToProcessAndDatabaseFilenames>; + + // Returns a two-part or three-part structure: + // + // The first part is an array of subdirectories to process. + // + // The second part is a hashtable of database filenames. + // + // When ObsoleteFilenames is ObsoleteFilenamesHandling::Include, will also + // collect files based on the marker files. For now, + // GetUsageForOriginInternal() is the only consumer of this result because it + // checks those unfinished deletion and clean them up after that. + template <ObsoleteFilenamesHandling ObsoleteFilenames = + ObsoleteFilenamesHandling::Omit> + Result<GetDatabaseFilenamesResult<ObsoleteFilenames>, nsresult> + GetDatabaseFilenames(nsIFile& aDirectory, const AtomicBool& aCanceled); + + nsresult GetUsageForOriginInternal(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled, + bool aInitializing, UsageInfo* aUsageInfo); + + // Runs on the PBackground thread. Checks to see if there's a queued + // Maintenance to run. + void ProcessMaintenanceQueue(); +}; + +class DeleteFilesRunnable final : public Runnable, + public OpenDirectoryListener { + using DirectoryLock = mozilla::dom::quota::DirectoryLock; + + enum State { + // Just created on the PBackground 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> mOwningEventTarget; + SafeRefPtr<DatabaseFileManager> mFileManager; + RefPtr<DirectoryLock> mDirectoryLock; + nsTArray<int64_t> mFileIds; + State mState; + + public: + DeleteFilesRunnable(SafeRefPtr<DatabaseFileManager> aFileManager, + nsTArray<int64_t>&& aFileIds); + + void RunImmediately(); + + private: + ~DeleteFilesRunnable() = default; + + void Open(); + + void DoDatabaseWork(); + + void Finish(); + + void UnblockOpen(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + + // OpenDirectoryListener overrides. + virtual void DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void DirectoryLockFailed() override; +}; + +class Maintenance final : public Runnable, public OpenDirectoryListener { + struct DirectoryInfo final { + InitializedOnce<const OriginMetadata> mOriginMetadata; + InitializedOnce<const nsTArray<nsString>> mDatabasePaths; + const PersistenceType mPersistenceType; + + DirectoryInfo(PersistenceType aPersistenceType, + OriginMetadata aOriginMetadata, + nsTArray<nsString>&& aDatabasePaths); + + DirectoryInfo(const DirectoryInfo& aOther) = delete; + DirectoryInfo(DirectoryInfo&& aOther) = delete; + + ~DirectoryInfo() { MOZ_COUNT_DTOR(Maintenance::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<UniversalDirectoryLock> mPendingDirectoryLock; + RefPtr<UniversalDirectoryLock> mDirectoryLock; + nsTArray<DirectoryInfo> mDirectoryInfos; + nsTHashMap<nsStringHashKey, DatabaseMaintenance*> mDatabaseMaintenances; + nsresult mResultCode; + Atomic<bool> mAborted; + State mState; + + public: + explicit Maintenance(QuotaClient* aQuotaClient) + : Runnable("dom::indexedDB::Maintenance"), + 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); + + RefPtr<DatabaseMaintenance> GetDatabaseMaintenance( + const nsAString& aDatabasePath) const { + AssertIsOnBackgroundThread(); + + return mDatabaseMaintenances.Get(aDatabasePath); + } + + void Stringify(nsACString& aResult) const; + + private: + ~Maintenance() override { + 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(); + + // We need to declare refcounting unconditionally, because + // OpenDirectoryListener has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIRUNNABLE + + // OpenDirectoryListener overrides. + void DirectoryLockAcquired(DirectoryLock* aLock) override; + + void DirectoryLockFailed() override; +}; + +Maintenance::DirectoryInfo::DirectoryInfo(PersistenceType aPersistenceType, + OriginMetadata aOriginMetadata, + nsTArray<nsString>&& aDatabasePaths) + : mOriginMetadata(std::move(aOriginMetadata)), + mDatabasePaths(std::move(aDatabasePaths)), + mPersistenceType(aPersistenceType) { + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID); + MOZ_ASSERT(!mOriginMetadata->mGroup.IsEmpty()); + MOZ_ASSERT(!mOriginMetadata->mOrigin.IsEmpty()); +#ifdef DEBUG + MOZ_ASSERT(!mDatabasePaths->IsEmpty()); + for (const nsAString& databasePath : *mDatabasePaths) { + MOZ_ASSERT(!databasePath.IsEmpty()); + } +#endif + + MOZ_COUNT_CTOR(Maintenance::DirectoryInfo); +} + +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; + RefPtr<DirectoryLock> mDirectoryLock; + const OriginMetadata mOriginMetadata; + const nsString mDatabasePath; + int64_t mDirectoryLockId; + nsCOMPtr<nsIRunnable> mCompleteCallback; + const PersistenceType mPersistenceType; + const Maybe<CipherKey> mMaybeKey; + + public: + DatabaseMaintenance(Maintenance* aMaintenance, DirectoryLock* aDirectoryLock, + PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const nsAString& aDatabasePath, + const Maybe<CipherKey>& aMaybeKey) + : Runnable("dom::indexedDB::DatabaseMaintenance"), + mMaintenance(aMaintenance), + mDirectoryLock(aDirectoryLock), + mOriginMetadata(aOriginMetadata), + mDatabasePath(aDatabasePath), + mPersistenceType(aPersistenceType), + mMaybeKey{aMaybeKey} { + MOZ_ASSERT(aDirectoryLock); + + MOZ_ASSERT(mDirectoryLock->Id() >= 0); + mDirectoryLockId = mDirectoryLock->Id(); + } + + const nsAString& DatabasePath() const { return mDatabasePath; } + + void WaitForCompletion(nsIRunnable* aCallback) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mCompleteCallback); + + mCompleteCallback = aCallback; + } + + void Stringify(nsACString& aResult) const; + + private: + ~DatabaseMaintenance() override = default; + + // 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; + LazyInitializedOnce<const NotNull<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() +#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(NotNull<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; +}; + +#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 + ******************************************************************************/ + +// XXX Get rid of FileHelper and move the functions into DatabaseFileManager. +// Then, DatabaseFileManager::Get(Journal)Directory and +// DatabaseFileManager::GetFileForId might eventually be made private. +class MOZ_STACK_CLASS FileHelper final { + const SafeRefPtr<DatabaseFileManager> mFileManager; + + LazyInitializedOnce<const NotNull<nsCOMPtr<nsIFile>>> mFileDirectory; + LazyInitializedOnce<const NotNull<nsCOMPtr<nsIFile>>> mJournalDirectory; + + class ReadCallback; + LazyInitializedOnce<const NotNull<RefPtr<ReadCallback>>> mReadCallback; + + public: + explicit FileHelper(SafeRefPtr<DatabaseFileManager>&& aFileManager) + : mFileManager(std::move(aFileManager)) { + MOZ_ASSERT(mFileManager); + } + + nsresult Init(); + + [[nodiscard]] nsCOMPtr<nsIFile> GetFile(const DatabaseFileInfo& aFileInfo); + + [[nodiscard]] nsCOMPtr<nsIFile> GetJournalFile( + const DatabaseFileInfo& aFileInfo); + + nsresult CreateFileFromStream(nsIFile& aFile, nsIFile& aJournalFile, + nsIInputStream& aInputStream, bool aCompress, + const Maybe<CipherKey>& aMaybeKey); + + private: + nsresult SyncCopy(nsIInputStream& aInputStream, + nsIOutputStream& aOutputStream, char* aBuffer, + uint32_t aBufferSize); + + nsresult SyncRead(nsIInputStream& aInputStream, char* aBuffer, + uint32_t aBufferSize, uint32_t* aRead); +}; + +/******************************************************************************* + * Helper Functions + ******************************************************************************/ + +bool GetFilenameBase(const nsAString& aFilename, const nsAString& aSuffix, + nsDependentSubstring& aFilenameBase) { + MOZ_ASSERT(!aFilename.IsEmpty()); + MOZ_ASSERT(aFilenameBase.IsEmpty()); + + if (!StringEndsWith(aFilename, aSuffix) || + aFilename.Length() == aSuffix.Length()) { + return false; + } + + MOZ_ASSERT(aFilename.Length() > aSuffix.Length()); + + aFilenameBase.Rebind(aFilename, 0, aFilename.Length() - aSuffix.Length()); + return true; +} + +class EncryptedFileBlobImpl final : public FileBlobImpl { + public: + EncryptedFileBlobImpl(const nsCOMPtr<nsIFile>& aNativeFile, + const DatabaseFileInfo::IdType aId, + const CipherKey& aKey) + : FileBlobImpl{aNativeFile}, mKey{aKey} { + SetFileId(aId); + } + + uint64_t GetSize(ErrorResult& aRv) override { + nsCOMPtr<nsIInputStream> inputStream; + CreateInputStream(getter_AddRefs(inputStream), aRv); + + if (aRv.Failed()) { + return 0; + } + + MOZ_ASSERT(inputStream); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(inputStream, Available), 0, + [&aRv](const nsresult rv) { aRv = rv; }); + } + + void CreateInputStream(nsIInputStream** aInputStream, + ErrorResult& aRv) const override { + nsCOMPtr<nsIInputStream> baseInputStream; + FileBlobImpl::CreateInputStream(getter_AddRefs(baseInputStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + *aInputStream = + MakeAndAddRef<DecryptingInputStream<IndexedDBCipherStrategy>>( + WrapNotNull(std::move(baseInputStream)), kEncryptedStreamBlockSize, + mKey) + .take(); + } + + void GetBlobImplType(nsAString& aBlobImplType) const override { + aBlobImplType = u"EncryptedFileBlobImpl"_ns; + } + + already_AddRefed<BlobImpl> CreateSlice(uint64_t aStart, uint64_t aLength, + const nsAString& aContentType, + ErrorResult& aRv) const override { + MOZ_CRASH("Not implemented because this should be unreachable."); + } + + private: + const CipherKey mKey; +}; + +RefPtr<BlobImpl> CreateFileBlobImpl(const Database& aDatabase, + const nsCOMPtr<nsIFile>& aNativeFile, + const DatabaseFileInfo::IdType aId) { + if (aDatabase.IsInPrivateBrowsing()) { + nsCString keyId; + keyId.AppendInt(aId); + + const auto& key = + aDatabase.GetFileManager().MutableCipherKeyManagerRef().Get(keyId); + + MOZ_RELEASE_ASSERT(key.isSome()); + return MakeRefPtr<EncryptedFileBlobImpl>(aNativeFile, aId, *key); + } + + auto impl = MakeRefPtr<FileBlobImpl>(aNativeFile); + impl->SetFileId(aId); + + return impl; +} + +Result<nsTArray<SerializedStructuredCloneFile>, nsresult> +SerializeStructuredCloneFiles(const SafeRefPtr<Database>& aDatabase, + const nsTArray<StructuredCloneFileParent>& aFiles, + bool aForPreprocess) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + + if (aFiles.IsEmpty()) { + return nsTArray<SerializedStructuredCloneFile>{}; + } + + const nsCOMPtr<nsIFile> directory = + aDatabase->GetFileManager().GetCheckedDirectory(); + QM_TRY(OkIf(directory), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + nsTArray<SerializedStructuredCloneFile> serializedStructuredCloneFiles; + QM_TRY(OkIf(serializedStructuredCloneFiles.SetCapacity(aFiles.Length(), + fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + QM_TRY(TransformIfAbortOnErr( + aFiles, MakeBackInserter(serializedStructuredCloneFiles), + [aForPreprocess](const auto& file) { + return !aForPreprocess || + file.Type() == StructuredCloneFileBase::eStructuredClone; + }, + [&directory, &aDatabase, aForPreprocess]( + const auto& file) -> Result<SerializedStructuredCloneFile, nsresult> { + const int64_t fileId = file.FileInfo().Id(); + MOZ_ASSERT(fileId > 0); + + const nsCOMPtr<nsIFile> nativeFile = + mozilla::dom::indexedDB::DatabaseFileManager::GetCheckedFileForId( + directory, fileId); + QM_TRY(OkIf(nativeFile), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + switch (file.Type()) { + case StructuredCloneFileBase::eStructuredClone: + if (!aForPreprocess) { + return SerializedStructuredCloneFile{ + null_t(), StructuredCloneFileBase::eStructuredClone}; + } + + [[fallthrough]]; + + case StructuredCloneFileBase::eBlob: { + const auto impl = CreateFileBlobImpl(*aDatabase, nativeFile, + file.FileInfo().Id()); + + IPCBlob ipcBlob; + + // This can only fail if the child has crashed. + QM_TRY(MOZ_TO_RESULT(IPCBlobUtils::Serialize(impl, ipcBlob)), + Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + aDatabase->MapBlob(ipcBlob, file.FileInfoPtr()); + + return SerializedStructuredCloneFile{ipcBlob, file.Type()}; + } + + case StructuredCloneFileBase::eMutableFile: + case StructuredCloneFileBase::eWasmBytecode: + case StructuredCloneFileBase::eWasmCompiled: { + // Set file() to null, support for storing WebAssembly.Modules has + // been removed in bug 1469395. Support for de-serialization of + // WebAssembly.Modules modules has been removed in bug 1561876. + // Support for MutableFile has been removed in bug 1500343. Full + // removal is tracked in bug 1487479. + + return SerializedStructuredCloneFile{null_t(), file.Type()}; + } + + default: + MOZ_CRASH("Should never get here!"); + } + })); + + return std::move(serializedStructuredCloneFiles); +} + +bool IsFileNotFoundError(const nsresult aRv) { + return aRv == NS_ERROR_FILE_NOT_FOUND; +} + +enum struct Idempotency { Yes, No }; + +// Delete a file, decreasing the quota usage as appropriate. If the file no +// longer exists but aIdempotency is Idempotency::Yes, success is returned, +// although quota usage can't be decreased. (With the assumption being that the +// file was already deleted prior to this logic running, and the non-existent +// file was no longer tracked by quota because it didn't exist at +// initialization time or a previous deletion call updated the usage.) +nsresult DeleteFile(nsIFile& aFile, QuotaManager* const aQuotaManager, + const PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const Idempotency aIdempotency) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + // Callers which pass Idempotency::Yes call this function without checking if + // the file already exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used + // here since we just want to log NS_ERROR_FILE_NOT_FOUND results and not spam + // the reports. + // Theoretically, there should be no QM_OR_ELSE_(WARN|LOG_VERBOSE)_IF when a + // caller passes Idempotency::No, but it's simpler when the predicate just + // always returns false in that case. + + const auto isIgnorableError = [&aIdempotency]() -> bool (*)(nsresult) { + if (aIdempotency == Idempotency::Yes) { + return IsFileNotFoundError; + } + + return [](const nsresult rv) { return false; }; + }(); + + QM_TRY_INSPECT( + const auto& fileSize, + ([aQuotaManager, &aFile, + isIgnorableError]() -> Result<Maybe<int64_t>, nsresult> { + if (aQuotaManager) { + QM_TRY_INSPECT( + const Maybe<int64_t>& fileSize, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize) + .map([](const int64_t val) { return Some(val); }), + // Predicate. + isIgnorableError, + // Fallback. + ErrToDefaultOk<Maybe<int64_t>>)); + + // XXX Can we really assert that the file size is not 0 if + // it existed? This might be violated by external + // influences. + MOZ_ASSERT(!fileSize || fileSize.value() >= 0); + + return fileSize; + } + + return Some(int64_t(0)); + }())); + + if (!fileSize) { + return NS_OK; + } + + QM_TRY_INSPECT(const auto& didExist, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT(aFile.Remove(false)).map(Some<Ok>), + // Predicate. + isIgnorableError, + // Fallback. + ErrToDefaultOk<Maybe<Ok>>)); + + if (!didExist) { + // XXX If we get here, this means that the file still existed when we + // queried its size, but no longer when we tried to remove it. Not sure if + // this should really be silently accepted in idempotent mode. + return NS_OK; + } + + if (fileSize.value() > 0) { + MOZ_ASSERT(aQuotaManager); + + aQuotaManager->DecreaseUsageForClient( + ClientMetadata{aOriginMetadata, Client::IDB}, fileSize.value()); + } + + return NS_OK; +} + +nsresult DeleteFile(nsIFile& aDirectory, const nsAString& aFilename, + QuotaManager* const aQuotaManager, + const PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const Idempotency aIdempotent) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aFilename.IsEmpty()); + + QM_TRY_INSPECT(const auto& file, CloneFileAndAppend(aDirectory, aFilename)); + + return DeleteFile(*file, aQuotaManager, aPersistenceType, aOriginMetadata, + aIdempotent); +} + +// Delete files in a directory that you think exists. If the directory doesn't +// exist, an error will not be returned, but warning telemetry will be +// generated! So only call this on directories that you know exist (idempotent +// usage, but it's not recommended). +nsresult DeleteFilesNoQuota(nsIFile& aFile) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& didExist, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(aFile.Remove(true)).map(Some<Ok>), + // Predicate. + IsFileNotFoundError, + // Fallback. + ErrToDefaultOk<Maybe<Ok>>)); + + Unused << didExist; + + return NS_OK; +} + +nsresult DeleteFilesNoQuota(nsIFile* aDirectory, const nsAString& aFilename) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(!aFilename.IsEmpty()); + + // The current using function hasn't initialized the origin, so in here we + // don't update the size of origin. Adding this assertion for preventing from + // misusing. + DebugOnly<QuotaManager*> quotaManager = QuotaManager::Get(); + MOZ_ASSERT(!quotaManager->IsTemporaryStorageInitialized()); + + QM_TRY_INSPECT(const auto& file, CloneFileAndAppend(*aDirectory, aFilename)); + + QM_TRY(MOZ_TO_RESULT(DeleteFilesNoQuota(*file))); + + return NS_OK; +} + +// CreateMarkerFile and RemoveMarkerFile are a pair of functions to indicate +// whether having removed all the files successfully. The marker file should +// be checked before executing the next operation or initialization. +Result<nsCOMPtr<nsIFile>, nsresult> CreateMarkerFile( + nsIFile& aBaseDirectory, const nsAString& aDatabaseNameBase) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDatabaseNameBase.IsEmpty()); + + QM_TRY_INSPECT( + const auto& markerFile, + CloneFileAndAppend(aBaseDirectory, + kIdbDeletionMarkerFilePrefix + aDatabaseNameBase)); + + // Callers call this function without checking if the file already exists + // (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we just want + // to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the reports. + // + // TODO: In theory if this file exists, then RemoveDatabaseFilesAndDirectory + // should have cleaned it up, but obviously we can crash and not clean it up, + // which is the whole point of the marker file. In that case, we'll realize + // the marker file exists in OpenDatabaseOp::DoDatabaseWork or + // GetUsageForOriginInternal and resume the removal by calling + // RemoveDatabaseFilesAndDirectory again, but we will also try to create the + // marker file again, so if we see this marker file, it is part + // of our standard operating procedure to redundantly try and create the + // marker here. We currently treat this as idempotent usage, but we could + // add an additional argument to RemoveDatabaseFilesAndDirectory which would + // indicate that we are resuming an unfinished removal, so the marker already + // exists and doesn't have to be created, and change + // QM_OR_ELSE_LOG_VERBOSE_IF to QM_OR_ELSE_WARN_IF in the end. + QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT(markerFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)), + // Predicate. + IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>, + // Fallback. + ErrToDefaultOk<>)); + + return markerFile; +} + +nsresult RemoveMarkerFile(nsIFile* aMarkerFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(aMarkerFile); + + DebugOnly<bool> exists; + MOZ_ASSERT(NS_SUCCEEDED(aMarkerFile->Exists(&exists))); + MOZ_ASSERT(exists); + + QM_TRY(MOZ_TO_RESULT(aMarkerFile->Remove(false))); + + return NS_OK; +} + +Result<Ok, nsresult> DeleteFileManagerDirectory( + nsIFile& aFileManagerDirectory, QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata) { + // XXX In theory, deleting can continue for other files in case of a failure, + // leaving only those files behind that cause the problem actually. However, + // the current architecture doesn't allow having more databases (for the same + // name) on disk, so trying to delete as much as possible won't help much + // because we need to delete entire .files directory in the end anyway. + QM_TRY(DatabaseFileManager::TraverseFiles( + aFileManagerDirectory, + // KnownDirEntryOp + [&aQuotaManager, aPersistenceType, &aOriginMetadata]( + nsIFile& file, const bool isDirectory) -> Result<Ok, nsresult> { + if (isDirectory) { + // The journal directory doesn't count towards quota. + QM_TRY_RETURN(MOZ_TO_RESULT(DeleteFilesNoQuota(file))); + } + + // Stored files do count towards quota. + QM_TRY_RETURN( + MOZ_TO_RESULT(DeleteFile(file, aQuotaManager, aPersistenceType, + aOriginMetadata, Idempotency::Yes))); + }, + // UnknownDirEntryOp + [aPersistenceType, &aOriginMetadata]( + nsIFile& file, const bool isDirectory) -> Result<Ok, nsresult> { + // Unknown files and directories don't count towards quota. + + if (isDirectory) { + QM_TRY_RETURN(MOZ_TO_RESULT(DeleteFilesNoQuota(file))); + } + + QM_TRY_RETURN(MOZ_TO_RESULT( + DeleteFile(file, /* doesn't count */ nullptr, aPersistenceType, + aOriginMetadata, Idempotency::Yes))); + })); + + QM_TRY_RETURN(MOZ_TO_RESULT(aFileManagerDirectory.Remove(false))); +} + +// Idempotently delete all the parts of an IndexedDB database including its +// SQLite database file, its WAL journal, it's shared-memory file, and its +// Blob/Files sub-directory. A marker file is created prior to performing the +// deletion so that in the event we crash or fail to successfully delete the +// database and its files, we will re-attempt the deletion the next time the +// origin is initialized using this method. Because this means the method may be +// called on a partially deleted database, this method uses DeleteFile which +// succeeds when the file we ask it to delete does not actually exist. The +// marker file is removed once deletion has successfully completed. +nsresult RemoveDatabaseFilesAndDirectory(nsIFile& aBaseDirectory, + const nsAString& aDatabaseFilenameBase, + QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + const nsAString& aDatabaseName) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDatabaseFilenameBase.IsEmpty()); + + AUTO_PROFILER_LABEL("RemoveDatabaseFilesAndDirectory", DOM); + + QM_TRY_UNWRAP(auto markerFile, + CreateMarkerFile(aBaseDirectory, aDatabaseFilenameBase)); + + // The database file counts towards quota. + QM_TRY(MOZ_TO_RESULT(DeleteFile( + aBaseDirectory, aDatabaseFilenameBase + kSQLiteSuffix, aQuotaManager, + aPersistenceType, aOriginMetadata, Idempotency::Yes))); + + // .sqlite-journal files don't count towards quota. + QM_TRY(MOZ_TO_RESULT(DeleteFile(aBaseDirectory, + aDatabaseFilenameBase + kSQLiteJournalSuffix, + /* doesn't count */ nullptr, aPersistenceType, + aOriginMetadata, Idempotency::Yes))); + + // .sqlite-shm files don't count towards quota. + QM_TRY(MOZ_TO_RESULT(DeleteFile(aBaseDirectory, + aDatabaseFilenameBase + kSQLiteSHMSuffix, + /* doesn't count */ nullptr, aPersistenceType, + aOriginMetadata, Idempotency::Yes))); + + // .sqlite-wal files do count towards quota. + QM_TRY(MOZ_TO_RESULT(DeleteFile( + aBaseDirectory, aDatabaseFilenameBase + kSQLiteWALSuffix, aQuotaManager, + aPersistenceType, aOriginMetadata, Idempotency::Yes))); + + // The files directory counts towards quota. + QM_TRY_INSPECT( + const auto& fmDirectory, + CloneFileAndAppend(aBaseDirectory, aDatabaseFilenameBase + + kFileManagerDirectoryNameSuffix)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(fmDirectory, Exists)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(fmDirectory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + + QM_TRY(DeleteFileManagerDirectory(*fmDirectory, aQuotaManager, + aPersistenceType, aOriginMetadata)); + } + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get(); + MOZ_ASSERT_IF(aQuotaManager, mgr); + + if (mgr) { + mgr->InvalidateFileManager(aPersistenceType, aOriginMetadata.mOrigin, + aDatabaseName); + } + + QM_TRY(MOZ_TO_RESULT(RemoveMarkerFile(markerFile))); + + return NS_OK; +} + +/******************************************************************************* + * Globals + ******************************************************************************/ + +// Counts the number of "live" Factory, FactoryOp and Database instances. +uint64_t gBusyCount = 0; + +using FactoryOpArray = nsTArray<CheckedUnsafePtr<FactoryOp>>; + +StaticAutoPtr<FactoryOpArray> gFactoryOps; + +// Maps a database id to information about live database actors. +using DatabaseActorHashtable = + nsClassHashtable<nsCStringHashKey, DatabaseActorInfo>; + +StaticAutoPtr<DatabaseActorHashtable> gLiveDatabaseHashtable; + +StaticRefPtr<ConnectionPool> gConnectionPool; + +using DatabaseLoggingInfoHashtable = + nsTHashMap<nsIDHashKey, DatabaseLoggingInfo*>; + +StaticAutoPtr<DatabaseLoggingInfoHashtable> gLoggingInfoHashtable; + +using TelemetryIdHashtable = nsTHashMap<nsUint32HashKey, uint32_t>; + +StaticAutoPtr<TelemetryIdHashtable> gTelemetryIdHashtable; + +// Protects all reads and writes to gTelemetryIdHashtable. +StaticAutoPtr<Mutex> gTelemetryIdMutex; + +// For private browsing, maps the raw database names provided by content to a +// replacement UUID in order to avoid exposing the name of the database on +// disk or a directly derived value, such as the non-private-browsing +// representation. This mapping will be the same for all databases with the +// same name across all storage keys/origins for the lifetime of the IDB +// QuotaClient. In tests, the QuotaClient may be created and destroyed multiple +// times, but for normal browser use the QuotaClient will last until the +// browser shuts down. Bug 1831835 will improve this implementation to avoid +// using the same mapping across storage keys and to deal with the resulting +// lifecycle issues of the additional memory use. +using StorageDatabaseNameHashtable = nsTHashMap<nsString, nsString>; + +StaticAutoPtr<StorageDatabaseNameHashtable> gStorageDatabaseNameHashtable; + +// Protects all reads and writes to gStorageDatabaseNameHashtable. +StaticAutoPtr<Mutex> gStorageDatabaseNameMutex; + +#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 + } +} + +template <typename Condition> +void InvalidateLiveDatabasesMatching(const Condition& aCondition) { + AssertIsOnBackgroundThread(); + + if (!gLiveDatabaseHashtable) { + return; + } + + // Invalidating a Database will cause it to be removed from the + // gLiveDatabaseHashtable entries' mLiveDatabases, and, if it was the last + // element in mLiveDatabases, to remove the whole hashtable entry. Therefore, + // we need to make a temporary list of the databases to invalidate to avoid + // iterator invalidation. + + nsTArray<SafeRefPtr<Database>> databases; + + for (const auto& liveDatabasesEntry : gLiveDatabaseHashtable->Values()) { + for (const auto& database : liveDatabasesEntry->mLiveDatabases) { + if (aCondition(*database)) { + databases.AppendElement( + SafeRefPtr{database.get(), AcquireStrongRefFromRawPtr{}}); + } + } + } + + for (const auto& database : databases) { + database->Invalidate(); + } +} + +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. + MOZ_ASSERT(StringEndsWith(filename, kSQLiteSuffix)); + + filename.Truncate(filename.Length() - kSQLiteSuffix.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)); + + constexpr auto separator = u"*"_ns; + + uint32_t hashValue = + HashString(persistence + separator + origin + separator + filename); + + MutexAutoLock lock(*gTelemetryIdMutex); + + if (!gTelemetryIdHashtable) { + gTelemetryIdHashtable = new TelemetryIdHashtable(); + } + + return gTelemetryIdHashtable->LookupOrInsertWith(hashValue, [] { + static uint32_t sNextId = 1; + + // We're locked, no need for atomics. + return sNextId++; + }); +} + +nsAutoString GetDatabaseFilenameBase(const nsAString& aDatabaseName, + bool aIsPrivate) { + nsAutoString databaseFilenameBase; + + if (aIsPrivate) { + MOZ_DIAGNOSTIC_ASSERT(gStorageDatabaseNameMutex); + + MutexAutoLock lock(*gStorageDatabaseNameMutex); + + if (!gStorageDatabaseNameHashtable) { + gStorageDatabaseNameHashtable = new StorageDatabaseNameHashtable(); + } + + databaseFilenameBase.Append( + gStorageDatabaseNameHashtable->LookupOrInsertWith(aDatabaseName, []() { + return NSID_TrimBracketsUTF16(nsID::GenerateUUID()); + })); + + return databaseFilenameBase; + } + + // WARNING: do not change this hash function. See the comment in HashName() + // for details. + databaseFilenameBase.AppendInt(HashName(aDatabaseName)); + + nsAutoCString escapedName; + if (!NS_Escape(NS_ConvertUTF16toUTF8(aDatabaseName), 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++); + } + } + + databaseFilenameBase.AppendASCII(substring.get(), substring.Length()); + + return databaseFilenameBase; +} + +const CommonIndexOpenCursorParams& GetCommonIndexOpenCursorParams( + const OpenCursorParams& aParams) { + switch (aParams.type()) { + case OpenCursorParams::TIndexOpenCursorParams: + return aParams.get_IndexOpenCursorParams().commonIndexParams(); + case OpenCursorParams::TIndexOpenKeyCursorParams: + return aParams.get_IndexOpenKeyCursorParams().commonIndexParams(); + default: + MOZ_CRASH("Should never get here!"); + } +} + +const CommonOpenCursorParams& GetCommonOpenCursorParams( + const OpenCursorParams& aParams) { + switch (aParams.type()) { + case OpenCursorParams::TObjectStoreOpenCursorParams: + return aParams.get_ObjectStoreOpenCursorParams().commonParams(); + case OpenCursorParams::TObjectStoreOpenKeyCursorParams: + return aParams.get_ObjectStoreOpenKeyCursorParams().commonParams(); + case OpenCursorParams::TIndexOpenCursorParams: + case OpenCursorParams::TIndexOpenKeyCursorParams: + return GetCommonIndexOpenCursorParams(aParams).commonParams(); + default: + MOZ_CRASH("Should never get here!"); + } +} + +// TODO: Using nsCString as a return type here seems to lead to a dependency on +// some temporaries, which I did not expect. Is it a good idea that the default +// operator+ behaviour constructs such strings? It is certainly useful as an +// optimization, but this should be better done via an appropriately named +// function rather than an operator. +nsAutoCString MakeColumnPairSelectionList( + const nsLiteralCString& aPlainColumnName, + const nsLiteralCString& aLocaleAwareColumnName, + const nsLiteralCString& aSortColumnAlias, const bool aIsLocaleAware) { + return aPlainColumnName + + (aIsLocaleAware ? EmptyCString() : " as "_ns + aSortColumnAlias) + + ", "_ns + aLocaleAwareColumnName + + (aIsLocaleAware ? " as "_ns + aSortColumnAlias : EmptyCString()); +} + +constexpr bool IsIncreasingOrder(const IDBCursorDirection aDirection) { + MOZ_ASSERT(aDirection == IDBCursorDirection::Next || + aDirection == IDBCursorDirection::Nextunique || + aDirection == IDBCursorDirection::Prev || + aDirection == IDBCursorDirection::Prevunique); + + return aDirection == IDBCursorDirection::Next || + aDirection == IDBCursorDirection::Nextunique; +} + +constexpr bool IsUnique(const IDBCursorDirection aDirection) { + MOZ_ASSERT(aDirection == IDBCursorDirection::Next || + aDirection == IDBCursorDirection::Nextunique || + aDirection == IDBCursorDirection::Prev || + aDirection == IDBCursorDirection::Prevunique); + + return aDirection == IDBCursorDirection::Nextunique || + aDirection == IDBCursorDirection::Prevunique; +} + +// TODO: In principle, this could be constexpr, if operator+(nsLiteralCString, +// nsLiteralCString) were constexpr and returned a literal type. +nsAutoCString MakeDirectionClause(const IDBCursorDirection aDirection) { + return " ORDER BY "_ns + kColumnNameKey + + (IsIncreasingOrder(aDirection) ? " ASC"_ns : " DESC"_ns); +} + +enum struct ComparisonOperator { + LessThan, + LessOrEquals, + Equals, + GreaterThan, + GreaterOrEquals, +}; + +constexpr nsLiteralCString GetComparisonOperatorString( + const ComparisonOperator aComparisonOperator) { + switch (aComparisonOperator) { + case ComparisonOperator::LessThan: + return "<"_ns; + case ComparisonOperator::LessOrEquals: + return "<="_ns; + case ComparisonOperator::Equals: + return "=="_ns; + case ComparisonOperator::GreaterThan: + return ">"_ns; + case ComparisonOperator::GreaterOrEquals: + return ">="_ns; + } + + // TODO: This is just to silence the "control reaches end of non-void + // function" warning. Cannot use MOZ_CRASH in a constexpr function, + // unfortunately. + return ""_ns; +} + +nsAutoCString GetKeyClause(const nsACString& aColumnName, + const ComparisonOperator aComparisonOperator, + const nsLiteralCString& aStmtParamName) { + return aColumnName + " "_ns + + GetComparisonOperatorString(aComparisonOperator) + " :"_ns + + aStmtParamName; +} + +nsAutoCString GetSortKeyClause(const ComparisonOperator aComparisonOperator, + const nsLiteralCString& aStmtParamName) { + return GetKeyClause(kColumnNameAliasSortKey, aComparisonOperator, + aStmtParamName); +} + +template <IDBCursorType CursorType> +struct PopulateResponseHelper; + +struct CommonPopulateResponseHelper { + explicit CommonPopulateResponseHelper( + const TransactionDatabaseOperationBase& aOp) + : mOp{aOp} {} + + nsresult GetKeys(mozIStorageStatement* const aStmt, + Key* const aOptOutSortKey) { + QM_TRY(MOZ_TO_RESULT(GetCommonKeys(aStmt))); + + if (aOptOutSortKey) { + *aOptOutSortKey = mPosition; + } + + return NS_OK; + } + + nsresult GetCommonKeys(mozIStorageStatement* const aStmt) { + MOZ_ASSERT(mPosition.IsUnset()); + + QM_TRY(MOZ_TO_RESULT(mPosition.SetFromStatement(aStmt, 0))); + + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "PRELOAD: Populating response with key %s", "Populating%.0s", + IDB_LOG_ID_STRING(mOp.BackgroundChildLoggingId()), + mOp.TransactionLoggingSerialNumber(), mOp.LoggingSerialNumber(), + mPosition.GetBuffer().get()); + + return NS_OK; + } + + template <typename Response> + void FillKeys(Response& aResponse) { + MOZ_ASSERT(!mPosition.IsUnset()); + aResponse.key() = std::move(mPosition); + } + + template <typename Response> + static size_t GetKeySize(const Response& aResponse) { + return aResponse.key().GetBuffer().Length(); + } + + protected: + const Key& GetPosition() const { return mPosition; } + + private: + const TransactionDatabaseOperationBase& mOp; + Key mPosition; +}; + +struct IndexPopulateResponseHelper : CommonPopulateResponseHelper { + using CommonPopulateResponseHelper::CommonPopulateResponseHelper; + + nsresult GetKeys(mozIStorageStatement* const aStmt, + Key* const aOptOutSortKey) { + MOZ_ASSERT(mLocaleAwarePosition.IsUnset()); + MOZ_ASSERT(mObjectStorePosition.IsUnset()); + + QM_TRY(MOZ_TO_RESULT(CommonPopulateResponseHelper::GetCommonKeys(aStmt))); + + QM_TRY(MOZ_TO_RESULT(mLocaleAwarePosition.SetFromStatement(aStmt, 1))); + + QM_TRY(MOZ_TO_RESULT(mObjectStorePosition.SetFromStatement(aStmt, 2))); + + if (aOptOutSortKey) { + *aOptOutSortKey = + mLocaleAwarePosition.IsUnset() ? GetPosition() : mLocaleAwarePosition; + } + + return NS_OK; + } + + template <typename Response> + void FillKeys(Response& aResponse) { + MOZ_ASSERT(!mLocaleAwarePosition.IsUnset()); + MOZ_ASSERT(!mObjectStorePosition.IsUnset()); + + CommonPopulateResponseHelper::FillKeys(aResponse); + aResponse.sortKey() = std::move(mLocaleAwarePosition); + aResponse.objectKey() = std::move(mObjectStorePosition); + } + + template <typename Response> + static size_t GetKeySize(Response& aResponse) { + return CommonPopulateResponseHelper::GetKeySize(aResponse) + + aResponse.sortKey().GetBuffer().Length() + + aResponse.objectKey().GetBuffer().Length(); + } + + private: + Key mLocaleAwarePosition, mObjectStorePosition; +}; + +struct KeyPopulateResponseHelper { + static constexpr nsresult MaybeGetCloneInfo( + mozIStorageStatement* const /*aStmt*/, const CursorBase& /*aCursor*/) { + return NS_OK; + } + + template <typename Response> + static constexpr void MaybeFillCloneInfo(Response& /*aResponse*/, + FilesArray* const /*aFiles*/) {} + + template <typename Response> + static constexpr size_t MaybeGetCloneInfoSize(const Response& /*aResponse*/) { + return 0; + } +}; + +template <bool StatementHasIndexKeyBindings> +struct ValuePopulateResponseHelper { + nsresult MaybeGetCloneInfo(mozIStorageStatement* const aStmt, + const ValueCursorBase& aCursor) { + constexpr auto offset = StatementHasIndexKeyBindings ? 2 : 0; + + QM_TRY_UNWRAP(auto cloneInfo, + GetStructuredCloneReadInfoFromStatement( + aStmt, 2 + offset, 1 + offset, *aCursor.mFileManager)); + + mCloneInfo.init(std::move(cloneInfo)); + + if (mCloneInfo->HasPreprocessInfo()) { + IDB_WARNING("Preprocessing for cursors not yet implemented!"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + return NS_OK; + } + + template <typename Response> + void MaybeFillCloneInfo(Response& aResponse, FilesArray* const aFiles) { + auto cloneInfo = mCloneInfo.release(); + aResponse.cloneInfo().data().data = cloneInfo.ReleaseData(); + aFiles->AppendElement(cloneInfo.ReleaseFiles()); + } + + template <typename Response> + static size_t MaybeGetCloneInfoSize(const Response& aResponse) { + return aResponse.cloneInfo().data().data.Size(); + } + + private: + LazyInitializedOnceEarlyDestructible<const StructuredCloneReadInfoParent> + mCloneInfo; +}; + +template <> +struct PopulateResponseHelper<IDBCursorType::ObjectStore> + : ValuePopulateResponseHelper<false>, CommonPopulateResponseHelper { + using CommonPopulateResponseHelper::CommonPopulateResponseHelper; + + static auto& GetTypedResponse(CursorResponse* const aResponse) { + return aResponse->get_ArrayOfObjectStoreCursorResponse(); + } +}; + +template <> +struct PopulateResponseHelper<IDBCursorType::ObjectStoreKey> + : KeyPopulateResponseHelper, CommonPopulateResponseHelper { + using CommonPopulateResponseHelper::CommonPopulateResponseHelper; + + static auto& GetTypedResponse(CursorResponse* const aResponse) { + return aResponse->get_ArrayOfObjectStoreKeyCursorResponse(); + } +}; + +template <> +struct PopulateResponseHelper<IDBCursorType::Index> + : ValuePopulateResponseHelper<true>, IndexPopulateResponseHelper { + using IndexPopulateResponseHelper::IndexPopulateResponseHelper; + + static auto& GetTypedResponse(CursorResponse* const aResponse) { + return aResponse->get_ArrayOfIndexCursorResponse(); + } +}; + +template <> +struct PopulateResponseHelper<IDBCursorType::IndexKey> + : KeyPopulateResponseHelper, IndexPopulateResponseHelper { + using IndexPopulateResponseHelper::IndexPopulateResponseHelper; + + static auto& GetTypedResponse(CursorResponse* const aResponse) { + return aResponse->get_ArrayOfIndexKeyCursorResponse(); + } +}; + +nsresult DispatchAndReturnFileReferences( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseName, const int64_t aFileId, + int32_t* const aMemRefCnt, int32_t* const aDBRefCnt, bool* const aResult) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aMemRefCnt); + MOZ_ASSERT(aDBRefCnt); + MOZ_ASSERT(aResult); + + *aResult = false; + *aMemRefCnt = -1; + *aDBRefCnt = -1; + + mozilla::Monitor monitor MOZ_ANNOTATED(__func__); + bool waiting = true; + + auto lambda = [&] { + AssertIsOnIOThread(); + + { + IndexedDatabaseManager* const mgr = IndexedDatabaseManager::Get(); + MOZ_ASSERT(mgr); + + const SafeRefPtr<DatabaseFileManager> fileManager = + mgr->GetFileManager(aPersistenceType, aOrigin, aDatabaseName); + + if (fileManager) { + const SafeRefPtr<DatabaseFileInfo> fileInfo = + fileManager->GetFileInfo(aFileId); + + if (fileInfo) { + fileInfo->GetReferences(aMemRefCnt, aDBRefCnt); + + if (*aMemRefCnt != -1) { + // We added an extra temp ref, so account for that accordingly. + (*aMemRefCnt)--; + } + + *aResult = true; + } + } + } + + mozilla::MonitorAutoLock lock(monitor); + MOZ_ASSERT(waiting); + + waiting = false; + lock.Notify(); + }; + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // XXX can't we simply use NS_DispatchAndSpinEventLoopUntilComplete instead of + // using a monitor? + QM_TRY(MOZ_TO_RESULT(quotaManager->IOThread()->Dispatch( + NS_NewRunnableFunction("GetFileReferences", std::move(lambda)), + NS_DISPATCH_NORMAL))); + + mozilla::MonitorAutoLock autolock(monitor); + while (waiting) { + autolock.Wait(); + } + + return NS_OK; +} + +class DeserializeIndexValueHelper final : public Runnable { + public: + DeserializeIndexValueHelper(int64_t aIndexID, const KeyPath& aKeyPath, + bool aMultiEntry, const nsACString& aLocale, + StructuredCloneReadInfoParent& aCloneReadInfo, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray) + : Runnable("DeserializeIndexValueHelper"), + mMonitor("DeserializeIndexValueHelper::mMonitor"), + mIndexID(aIndexID), + mKeyPath(aKeyPath), + mMultiEntry(aMultiEntry), + mLocale(aLocale), + mCloneReadInfo(aCloneReadInfo), + mUpdateInfoArray(aUpdateInfoArray), + mStatus(NS_ERROR_FAILURE) {} + + nsresult DispatchAndWait() { + // FIXME(Bug 1637530) Re-enable optimization using a non-system-principaled + // JS context +#if 0 + // We don't need to go to the main-thread and use the sandbox. Let's create + // the updateInfo data here. + if (!mCloneReadInfo.Data().Size()) { + AutoJSAPI jsapi; + jsapi.Init(); + + JS::Rooted<JS::Value> value(jsapi.cx()); + value.setUndefined(); + + ErrorResult rv; + IDBObjectStore::AppendIndexUpdateInfo(mIndexID, mKeyPath, mMultiEntry, + mLocale, jsapi.cx(), value, + &mUpdateInfoArray, &rv); + return rv.Failed() ? rv.StealNSResult() : NS_OK; + } +#endif + + // The operation will continue on the main-thread. + + MOZ_ASSERT(!(mCloneReadInfo.Data().Size() % sizeof(uint64_t))); + + MonitorAutoLock lock(mMonitor); + + RefPtr<Runnable> self = this; + QM_TRY(MOZ_TO_RESULT( + SchedulerGroup::Dispatch(TaskCategory::Other, self.forget()))); + + lock.Wait(); + return mStatus; + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* const cx = jsapi.cx(); + + JS::Rooted<JSObject*> global(cx, GetSandbox(cx)); + + QM_TRY(OkIf(global), NS_OK, + [this](const NotOk) { OperationCompleted(NS_ERROR_FAILURE); }); + + const JSAutoRealm ar(cx, global); + + JS::Rooted<JS::Value> value(cx); + QM_TRY(MOZ_TO_RESULT(DeserializeIndexValue(cx, &value)), NS_OK, + [this](const nsresult rv) { OperationCompleted(rv); }); + + ErrorResult errorResult; + IDBObjectStore::AppendIndexUpdateInfo(mIndexID, mKeyPath, mMultiEntry, + mLocale, cx, value, &mUpdateInfoArray, + &errorResult); + QM_TRY(OkIf(!errorResult.Failed()), NS_OK, + ([this, &errorResult](const NotOk) { + OperationCompleted(errorResult.StealNSResult()); + })); + + OperationCompleted(NS_OK); + return NS_OK; + } + + private: + nsresult DeserializeIndexValue(JSContext* aCx, + JS::MutableHandle<JS::Value> aValue) { + static const JSStructuredCloneCallbacks callbacks = { + StructuredCloneReadCallback<StructuredCloneReadInfoParent>, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr}; + + if (!JS_ReadStructuredClone( + aCx, mCloneReadInfo.Data(), JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::DifferentProcessForIndexedDB, aValue, + JS::CloneDataPolicy(), &callbacks, &mCloneReadInfo)) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + return NS_OK; + } + + void OperationCompleted(nsresult aStatus) { + mStatus = aStatus; + + MonitorAutoLock lock(mMonitor); + lock.Notify(); + } + + Monitor mMonitor MOZ_UNANNOTATED; + + const int64_t mIndexID; + const KeyPath& mKeyPath; + const bool mMultiEntry; + const nsCString mLocale; + StructuredCloneReadInfoParent& mCloneReadInfo; + nsTArray<IndexUpdateInfo>& mUpdateInfoArray; + nsresult mStatus; +}; + +auto DeserializeIndexValueToUpdateInfos( + int64_t aIndexID, const KeyPath& aKeyPath, bool aMultiEntry, + const nsACString& aLocale, StructuredCloneReadInfoParent& aCloneReadInfo) { + MOZ_ASSERT(!NS_IsMainThread()); + + using ArrayType = AutoTArray<IndexUpdateInfo, 32>; + using ResultType = Result<ArrayType, nsresult>; + + ArrayType updateInfoArray; + const auto helper = MakeRefPtr<DeserializeIndexValueHelper>( + aIndexID, aKeyPath, aMultiEntry, aLocale, aCloneReadInfo, + updateInfoArray); + const nsresult rv = helper->DispatchAndWait(); + return NS_FAILED(rv) ? Err(rv) : ResultType{std::move(updateInfoArray)}; +} + +bool IsSome( + const Maybe<CachingDatabaseConnection::BorrowedStatement>& aMaybeStmt) { + return aMaybeStmt.isSome(); +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +already_AddRefed<PBackgroundIDBFactoryParent> AllocPBackgroundIDBFactoryParent( + const LoggingInfo& aLoggingInfo) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_AUUF_OR_WARN_IF(!aLoggingInfo.nextTransactionSerialNumber()) || + NS_AUUF_OR_WARN_IF( + !aLoggingInfo.nextVersionChangeTransactionSerialNumber()) || + NS_AUUF_OR_WARN_IF(!aLoggingInfo.nextRequestSerialNumber())) { + return nullptr; + } + + SafeRefPtr<Factory> actor = Factory::Create(aLoggingInfo); + MOZ_ASSERT(actor); + + return actor.forget(); +} + +bool RecvPBackgroundIDBFactoryConstructor( + PBackgroundIDBFactoryParent* aActor, + const LoggingInfo& /* aLoggingInfo */) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + 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(); + + if (QuotaClient* quotaClient = QuotaClient::GetInstance()) { + QM_WARNONLY_TRY(QM_TO_RESULT(quotaClient->FlushPendingFileDeletions())); + } + + return true; +} + +RefPtr<mozilla::dom::quota::Client> CreateQuotaClient() { + AssertIsOnBackgroundThread(); + + return MakeRefPtr<QuotaClient>(); +} + +nsresult DatabaseFileManager::AsyncDeleteFile(int64_t aFileId) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mFileInfos.Contains(aFileId)); + + QuotaClient* quotaClient = QuotaClient::GetInstance(); + if (quotaClient) { + QM_TRY(MOZ_TO_RESULT(quotaClient->AsyncDeleteFile(this, aFileId))); + } + + return NS_OK; +} + +/******************************************************************************* + * DatabaseConnection implementation + ******************************************************************************/ + +DatabaseConnection::DatabaseConnection( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection, + MovingNotNull<SafeRefPtr<DatabaseFileManager>> aFileManager) + : CachingDatabaseConnection(std::move(aStorageConnection)), + mFileManager(std::move(aFileManager)), + mInReadTransaction(false), + mInWriteTransaction(false) +#ifdef DEBUG + , + mDEBUGSavepointCount(0) +#endif +{ + AssertIsOnConnectionThread(); + MOZ_ASSERT(mFileManager); +} + +DatabaseConnection::~DatabaseConnection() { + MOZ_ASSERT(!mFileManager); + MOZ_ASSERT(!mUpdateRefcountFunction); + MOZ_DIAGNOSTIC_ASSERT(!mInWriteTransaction); + MOZ_ASSERT(!mDEBUGSavepointCount); +} + +nsresult DatabaseConnection::Init() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN;"_ns))); + + mInReadTransaction = true; + + return NS_OK; +} + +nsresult DatabaseConnection::BeginWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + MOZ_ASSERT(mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::BeginWriteTransaction", DOM); + + // Release our read locks. + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("ROLLBACK;"_ns))); + + mInReadTransaction = false; + + if (!mUpdateRefcountFunction) { + MOZ_ASSERT(mFileManager); + + RefPtr<UpdateRefcountFunction> function = + new UpdateRefcountFunction(this, **mFileManager); + + QM_TRY(MOZ_TO_RESULT(MutableStorageConnection().CreateFunction( + "update_refcount"_ns, + /* aNumArguments */ 2, function))); + + mUpdateRefcountFunction = std::move(function); + } + + // This one cannot obviously use ExecuteCachedStatement because of the custom + // error handling for Execute only. If only Execute can produce + // NS_ERROR_STORAGE_BUSY, we could actually use ExecuteCachedStatement and + // simplify this. + QM_TRY_INSPECT(const auto& beginStmt, + BorrowCachedStatement("BEGIN IMMEDIATE;"_ns)); + + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(beginStmt->Execute()), + // Predicate. + IsSpecificError<NS_ERROR_STORAGE_BUSY>, + // Fallback. + ([&beginStmt](nsresult rv) { + 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. + const 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; + } + } + + return MOZ_TO_RESULT(rv); + }))); + + mInWriteTransaction = true; + + return NS_OK; +} + +nsresult DatabaseConnection::CommitWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::CommitWriteTransaction", DOM); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("COMMIT;"_ns))); + + mInWriteTransaction = false; + return NS_OK; +} + +void DatabaseConnection::RollbackWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!mInReadTransaction); + MOZ_DIAGNOSTIC_ASSERT(HasStorageConnection()); + + AUTO_PROFILER_LABEL("DatabaseConnection::RollbackWriteTransaction", DOM); + + if (!mInWriteTransaction) { + return; + } + + QM_WARNONLY_TRY( + BorrowCachedStatement("ROLLBACK;"_ns) + .andThen([&self = *this](const auto& stmt) -> Result<Ok, nsresult> { + // This may fail if SQLite already rolled back the transaction + // so ignore any errors. + + // XXX ROLLBACK can fail quite normmally if a previous statement + // failed to execute successfully so SQLite rolled back the + // transaction already. However, if it failed because of some other + // reason, we could try to close the connection. + Unused << stmt->Execute(); + + self.mInWriteTransaction = false; + return Ok{}; + })); +} + +void DatabaseConnection::FinishWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::FinishWriteTransaction", DOM); + + if (mUpdateRefcountFunction) { + mUpdateRefcountFunction->Reset(); + } + + QM_WARNONLY_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN;"_ns)) + .andThen([&](const auto) -> Result<Ok, nsresult> { + mInReadTransaction = true; + return Ok{}; + })); +} + +nsresult DatabaseConnection::StartSavepoint() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + MOZ_ASSERT(mUpdateRefcountFunction); + MOZ_ASSERT(mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::StartSavepoint", DOM); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement(SAVEPOINT_CLAUSE))); + + mUpdateRefcountFunction->StartSavepoint(); + +#ifdef DEBUG + MOZ_ASSERT(mDEBUGSavepointCount < UINT32_MAX); + mDEBUGSavepointCount++; +#endif + + return NS_OK; +} + +nsresult DatabaseConnection::ReleaseSavepoint() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + MOZ_ASSERT(mUpdateRefcountFunction); + MOZ_ASSERT(mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::ReleaseSavepoint", DOM); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("RELEASE "_ns SAVEPOINT_CLAUSE))); + + mUpdateRefcountFunction->ReleaseSavepoint(); + +#ifdef DEBUG + MOZ_ASSERT(mDEBUGSavepointCount); + mDEBUGSavepointCount--; +#endif + + return NS_OK; +} + +nsresult DatabaseConnection::RollbackSavepoint() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + MOZ_ASSERT(mUpdateRefcountFunction); + MOZ_ASSERT(mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::RollbackSavepoint", DOM); + +#ifdef DEBUG + MOZ_ASSERT(mDEBUGSavepointCount); + mDEBUGSavepointCount--; +#endif + + mUpdateRefcountFunction->RollbackSavepoint(); + + QM_TRY_INSPECT(const auto& stmt, + BorrowCachedStatement("ROLLBACK TO "_ns SAVEPOINT_CLAUSE)); + + // 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); + + AUTO_PROFILER_LABEL("DatabaseConnection::CheckpointInternal", DOM); + + 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(");"); + + QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement(stmtString))); + + return NS_OK; +} + +void DatabaseConnection::DoIdleProcessing(bool aNeedsCheckpoint) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::DoIdleProcessing", DOM); + + CachingDatabaseConnection::CachedStatement freelistStmt; + const uint32_t freelistCount = [this, &freelistStmt] { + QM_TRY_RETURN(GetFreelistCount(freelistStmt), 0u); + }(); + + CachedStatement rollbackStmt; + CachedStatement beginStmt; + if (aNeedsCheckpoint || freelistCount) { + QM_TRY_UNWRAP(rollbackStmt, GetCachedStatement("ROLLBACK;"_ns), QM_VOID); + QM_TRY_UNWRAP(beginStmt, GetCachedStatement("BEGIN;"_ns), QM_VOID); + + // Release the connection's normal transaction. It's possible that it could + // fail, but that isn't a problem here. + Unused << rollbackStmt.Borrow()->Execute(); + + mInReadTransaction = false; + } + + const bool freedSomePages = freelistCount && [this, &freelistStmt, + &rollbackStmt, freelistCount, + aNeedsCheckpoint] { + // Warn in case of an error, but do not propagate it. Just indicate we + // didn't free any pages. + QM_TRY_INSPECT(const bool& res, + ReclaimFreePagesWhileIdle(freelistStmt, rollbackStmt, + freelistCount, aNeedsCheckpoint), + false); + + // Make sure we didn't leave a transaction running. + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + return res; + }(); + + // Truncate the WAL if we were asked to or if we managed to free some space. + if (aNeedsCheckpoint || freedSomePages) { + QM_WARNONLY_TRY(QM_TO_RESULT(CheckpointInternal(CheckpointMode::Truncate))); + } + + // Finally try to restart the read transaction if we rolled it back earlier. + if (beginStmt) { + QM_WARNONLY_TRY( + MOZ_TO_RESULT(beginStmt.Borrow()->Execute()) + .andThen([&self = *this](const Ok) -> Result<Ok, nsresult> { + self.mInReadTransaction = true; + return Ok{}; + })); + } +} + +Result<bool, nsresult> DatabaseConnection::ReclaimFreePagesWhileIdle( + CachedStatement& aFreelistStatement, CachedStatement& aRollbackStatement, + uint32_t aFreelistCount, bool aNeedsCheckpoint) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aFreelistStatement); + MOZ_ASSERT(aRollbackStatement); + MOZ_ASSERT(aFreelistCount); + MOZ_ASSERT(!mInReadTransaction); + MOZ_ASSERT(!mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::ReclaimFreePagesWhileIdle", DOM); + + // Make sure we don't keep working if anything else needs this thread. + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + if (NS_HasPendingEvents(currentThread)) { + return false; + } + + // Make all the statements we'll need up front. + + // 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. + QM_TRY_INSPECT( + const auto& incrementalVacuumStmt, + GetCachedStatement( + "PRAGMA incremental_vacuum("_ns + + IntToCString(std::max(uint64_t(1), uint64_t(aFreelistCount / 10))) + + ");"_ns)); + + QM_TRY_INSPECT(const auto& beginImmediateStmt, + GetCachedStatement("BEGIN IMMEDIATE;"_ns)); + + QM_TRY_INSPECT(const auto& commitStmt, GetCachedStatement("COMMIT;"_ns)); + + 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. + QM_TRY(MOZ_TO_RESULT(CheckpointInternal(CheckpointMode::Restart))); + } + + // Start the write transaction. + QM_TRY(MOZ_TO_RESULT(beginImmediateStmt.Borrow()->Execute())); + + mInWriteTransaction = true; + + bool freedSomePages = false, interrupted = false; + + const auto rollback = [&aRollbackStatement, this](const auto&) { + MOZ_ASSERT(mInWriteTransaction); + + // Something failed, make sure we roll everything back. + Unused << aRollbackStatement.Borrow()->Execute(); + + // XXX Is rollback infallible? Shouldn't we check the result? + + mInWriteTransaction = false; + }; + + uint64_t previousFreelistCount = (uint64_t)aFreelistCount + 1; + + QM_TRY(CollectWhile( + [&aFreelistCount, &previousFreelistCount, &interrupted, + currentThread]() -> Result<bool, nsresult> { + if (NS_HasPendingEvents(currentThread)) { + // Abort if something else wants to use the thread, and + // roll back this transaction. It's ok if we never make + // progress here because the idle service should + // eventually reclaim this space. + interrupted = true; + return false; + } + // If we were not able to free anything, we might either see + // a DB that has no auto-vacuum support at all or some other + // (hopefully temporary) condition that prevents vacuum from + // working. Just carry on in non-DEBUG. + bool madeProgress = previousFreelistCount != aFreelistCount; + previousFreelistCount = aFreelistCount; + MOZ_ASSERT(madeProgress); + QM_WARNONLY_TRY(MOZ_TO_RESULT(!madeProgress)); + return madeProgress && (aFreelistCount != 0); + }, + [&aFreelistStatement, &aFreelistCount, &incrementalVacuumStmt, + &freedSomePages, this]() -> mozilla::Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(incrementalVacuumStmt.Borrow()->Execute())); + + freedSomePages = true; + + QM_TRY_UNWRAP(aFreelistCount, + GetFreelistCount(aFreelistStatement)); + + return Ok{}; + }) + .andThen([&commitStmt, &freedSomePages, &interrupted, &rollback, + this](Ok) -> Result<Ok, nsresult> { + if (interrupted) { + rollback(Ok{}); + freedSomePages = false; + } + + if (freedSomePages) { + // Commit the write transaction. + QM_TRY(MOZ_TO_RESULT(commitStmt.Borrow()->Execute()), + QM_PROPAGATE, + [](const auto&) { NS_WARNING("Failed to commit!"); }); + + mInWriteTransaction = false; + } + + return Ok{}; + }), + QM_PROPAGATE, rollback); + + return freedSomePages; +} + +Result<uint32_t, nsresult> DatabaseConnection::GetFreelistCount( + CachedStatement& aCachedStatement) { + AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("DatabaseConnection::GetFreelistCount", DOM); + + if (!aCachedStatement) { + QM_TRY_UNWRAP(aCachedStatement, + GetCachedStatement("PRAGMA freelist_count;"_ns)); + } + + const auto borrowedStatement = aCachedStatement.Borrow(); + + QM_TRY_UNWRAP(const DebugOnly<bool> hasResult, + MOZ_TO_RESULT_INVOKE_MEMBER(&*borrowedStatement, ExecuteStep)); + + MOZ_ASSERT(hasResult); + + QM_TRY_INSPECT(const int32_t& freelistCount, + MOZ_TO_RESULT_INVOKE_MEMBER(*borrowedStatement, GetInt32, 0)); + + MOZ_ASSERT(freelistCount >= 0); + + return uint32_t(freelistCount); +} + +void DatabaseConnection::Close() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!mDEBUGSavepointCount); + MOZ_DIAGNOSTIC_ASSERT(!mInWriteTransaction); + + AUTO_PROFILER_LABEL("DatabaseConnection::Close", DOM); + + if (mUpdateRefcountFunction) { + MOZ_ALWAYS_SUCCEEDS( + MutableStorageConnection().RemoveFunction("update_refcount"_ns)); + mUpdateRefcountFunction = nullptr; + } + + CachingDatabaseConnection::Close(); + + mFileManager.destroy(); +} + +nsresult DatabaseConnection::DisableQuotaChecks() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(HasStorageConnection()); + + if (!mQuotaObject) { + MOZ_ASSERT(!mJournalQuotaObject); + + QM_TRY(MOZ_TO_RESULT(MutableStorageConnection().GetQuotaObjects( + getter_AddRefs(mQuotaObject), getter_AddRefs(mJournalQuotaObject)))); + + MOZ_ASSERT(mQuotaObject); + MOZ_ASSERT(mJournalQuotaObject); + } + + mQuotaObject->DisableQuotaCheck(); + mJournalQuotaObject->DisableQuotaCheck(); + + return NS_OK; +} + +void DatabaseConnection::EnableQuotaChecks() { + AssertIsOnConnectionThread(); + if (!mQuotaObject) { + MOZ_ASSERT(!mJournalQuotaObject); + + // DisableQuotaChecks failed earlier, so we don't need to enable quota + // checks again. + return; + } + + MOZ_ASSERT(mJournalQuotaObject); + + const RefPtr<QuotaObject> quotaObject = std::move(mQuotaObject); + const RefPtr<QuotaObject> journalQuotaObject = std::move(mJournalQuotaObject); + + quotaObject->EnableQuotaCheck(); + journalQuotaObject->EnableQuotaCheck(); + + QM_TRY_INSPECT(const int64_t& fileSize, GetFileSize(quotaObject->Path()), + QM_VOID); + QM_TRY_INSPECT(const int64_t& journalFileSize, + GetFileSize(journalQuotaObject->Path()), QM_VOID); + + DebugOnly<bool> result = journalQuotaObject->MaybeUpdateSize( + journalFileSize, /* aTruncate */ true); + MOZ_ASSERT(result); + + result = quotaObject->MaybeUpdateSize(fileSize, /* aTruncate */ true); + MOZ_ASSERT(result); +} + +Result<int64_t, nsresult> DatabaseConnection::GetFileSize( + const nsAString& aPath) { + MOZ_ASSERT(!aPath.IsEmpty()); + + QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath)); + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); + } + + return 0; +} + +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::Mode::ReadWrite || + mDEBUGTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush || + mDEBUGTransaction->GetMode() == IDBTransaction::Mode::Cleanup || + mDEBUGTransaction->GetMode() == IDBTransaction::Mode::VersionChange); + + QM_WARNONLY_TRY(QM_TO_RESULT(mConnection->RollbackSavepoint())); + } +} + +nsresult DatabaseConnection::AutoSavepoint::Start( + const TransactionBase& aTransaction) { + MOZ_ASSERT(aTransaction.GetMode() == IDBTransaction::Mode::ReadWrite || + aTransaction.GetMode() == IDBTransaction::Mode::ReadWriteFlush || + aTransaction.GetMode() == IDBTransaction::Mode::Cleanup || + aTransaction.GetMode() == IDBTransaction::Mode::VersionChange); + + DatabaseConnection* connection = aTransaction.GetDatabase().GetConnection(); + MOZ_ASSERT(connection); + connection->AssertIsOnConnectionThread(); + + // The previous operation failed to begin a write transaction and the + // following opertion jumped to the connection thread before the previous + // operation has updated its failure to the transaction. + if (!connection->GetUpdateRefcountFunction()) { + NS_WARNING( + "The connection was closed because the previous operation " + "failed!"); + return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } + + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(!mDEBUGTransaction); + + QM_TRY(MOZ_TO_RESULT(connection->StartSavepoint())); + + mConnection = connection; +#ifdef DEBUG + mDEBUGTransaction = &aTransaction; +#endif + + return NS_OK; +} + +nsresult DatabaseConnection::AutoSavepoint::Commit() { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mDEBUGTransaction); + + QM_TRY(MOZ_TO_RESULT(mConnection->ReleaseSavepoint())); + + mConnection = nullptr; +#ifdef DEBUG + mDEBUGTransaction = nullptr; +#endif + + return NS_OK; +} + +DatabaseConnection::UpdateRefcountFunction::UpdateRefcountFunction( + DatabaseConnection* const aConnection, DatabaseFileManager& aFileManager) + : mConnection(aConnection), + mFileManager(aFileManager), + mInSavepoint(false) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); +} + +nsresult DatabaseConnection::UpdateRefcountFunction::WillCommit() { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection->HasStorageConnection()); + + AUTO_PROFILER_LABEL("DatabaseConnection::UpdateRefcountFunction::WillCommit", + DOM); + + // The parameter names are not used, parameters are bound by index + // only locally in the same function. + auto update = + [updateStatement = LazyStatement{*mConnection, + "UPDATE file " + "SET refcount = refcount + :delta " + "WHERE id = :id"_ns}, + selectStatement = LazyStatement{*mConnection, + "SELECT id " + "FROM file " + "WHERE id = :id"_ns}, + insertStatement = + LazyStatement{ + *mConnection, + "INSERT INTO file (id, refcount) VALUES(:id, :delta)"_ns}, + this](int64_t aId, int32_t aDelta) mutable -> Result<Ok, nsresult> { + AUTO_PROFILER_LABEL( + "DatabaseConnection::UpdateRefcountFunction::WillCommit::Update", DOM); + { + QM_TRY_INSPECT(const auto& borrowedUpdateStatement, + updateStatement.Borrow()); + + QM_TRY( + MOZ_TO_RESULT(borrowedUpdateStatement->BindInt32ByIndex(0, aDelta))); + QM_TRY(MOZ_TO_RESULT(borrowedUpdateStatement->BindInt64ByIndex(1, aId))); + QM_TRY(MOZ_TO_RESULT(borrowedUpdateStatement->Execute())); + } + + QM_TRY_INSPECT( + const int32_t& rows, + MOZ_TO_RESULT_INVOKE_MEMBER(mConnection->MutableStorageConnection(), + GetAffectedRows)); + + if (rows > 0) { + QM_TRY_INSPECT( + const bool& hasResult, + selectStatement + .BorrowAndExecuteSingleStep( + [aId](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, aId))); + return Ok{}; + }) + .map(IsSome)); + + if (!hasResult) { + // Don't have to create the journal here, we can create all at once, + // just before commit + mJournalsToCreateBeforeCommit.AppendElement(aId); + } + + return Ok{}; + } + + QM_TRY_INSPECT(const auto& borrowedInsertStatement, + insertStatement.Borrow()); + + QM_TRY(MOZ_TO_RESULT(borrowedInsertStatement->BindInt64ByIndex(0, aId))); + QM_TRY(MOZ_TO_RESULT(borrowedInsertStatement->BindInt32ByIndex(1, aDelta))); + QM_TRY(MOZ_TO_RESULT(borrowedInsertStatement->Execute())); + + mJournalsToRemoveAfterCommit.AppendElement(aId); + + return Ok{}; + }; + + QM_TRY(CollectEachInRange( + mFileInfoEntries, [&update](const auto& entry) -> Result<Ok, nsresult> { + const auto delta = entry.GetData()->Delta(); + if (delta) { + QM_TRY(update(entry.GetKey(), delta)); + } + + return Ok{}; + })); + + QM_TRY(MOZ_TO_RESULT(CreateJournals())); + + return NS_OK; +} + +void DatabaseConnection::UpdateRefcountFunction::DidCommit() { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("DatabaseConnection::UpdateRefcountFunction::DidCommit", + DOM); + + for (const auto& entry : mFileInfoEntries.Values()) { + entry->MaybeUpdateDBRefs(); + } + + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveJournals(mJournalsToRemoveAfterCommit))); +} + +void DatabaseConnection::UpdateRefcountFunction::DidAbort() { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("DatabaseConnection::UpdateRefcountFunction::DidAbort", + DOM); + + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveJournals(mJournalsToRemoveAfterAbort))); +} + +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 (const auto& entry : mSavepointEntriesIndex.Values()) { + entry->DecBySavepointDelta(); + } + + mInSavepoint = false; + mSavepointEntriesIndex.Clear(); +} + +void DatabaseConnection::UpdateRefcountFunction::Reset() { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!mSavepointEntriesIndex.Count()); + MOZ_ASSERT(!mInSavepoint); + + mJournalsToCreateBeforeCommit.Clear(); + mJournalsToRemoveAfterCommit.Clear(); + mJournalsToRemoveAfterAbort.Clear(); + + // DatabaseFileInfo 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 (const auto& entry : mFileInfoEntries.Values()) { + // We need to move mFileInfo into a raw pointer in order to release it + // explicitly with aSyncDeleteFile == true. + DatabaseFileInfo* const fileInfo = entry->ReleaseFileInfo().forget().take(); + MOZ_ASSERT(fileInfo); + + fileInfo->Release(/* aSyncDeleteFile */ true); + } + + mFileInfoEntries.Clear(); +} + +nsresult DatabaseConnection::UpdateRefcountFunction::ProcessValue( + mozIStorageValueArray* aValues, int32_t aIndex, UpdateType aUpdateType) { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aValues); + + AUTO_PROFILER_LABEL( + "DatabaseConnection::UpdateRefcountFunction::ProcessValue", DOM); + + QM_TRY_INSPECT(const int32_t& type, + MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, aIndex)); + + if (type == mozIStorageValueArray::VALUE_TYPE_NULL) { + return NS_OK; + } + + QM_TRY_INSPECT(const auto& ids, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, aValues, GetString, aIndex)); + + QM_TRY_INSPECT(const auto& files, + DeserializeStructuredCloneFiles(mFileManager, ids)); + + for (const StructuredCloneFileParent& file : files) { + const int64_t id = file.FileInfo().Id(); + MOZ_ASSERT(id > 0); + + const auto entry = + WrapNotNull(mFileInfoEntries.GetOrInsertNew(id, file.FileInfoPtr())); + + if (mInSavepoint) { + mSavepointEntriesIndex.InsertOrUpdate(id, entry); + } + + switch (aUpdateType) { + case UpdateType::Increment: + entry->IncDeltas(mInSavepoint); + break; + case UpdateType::Decrement: + entry->DecDeltas(mInSavepoint); + break; + default: + MOZ_CRASH("Unknown update type!"); + } + } + + return NS_OK; +} + +nsresult DatabaseConnection::UpdateRefcountFunction::CreateJournals() { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL( + "DatabaseConnection::UpdateRefcountFunction::CreateJournals", DOM); + + const nsCOMPtr<nsIFile> journalDirectory = mFileManager.GetJournalDirectory(); + QM_TRY(OkIf(journalDirectory), NS_ERROR_FAILURE); + + for (const int64_t id : mJournalsToCreateBeforeCommit) { + const nsCOMPtr<nsIFile> file = + DatabaseFileManager::GetFileForId(journalDirectory, id); + QM_TRY(OkIf(file), NS_ERROR_FAILURE); + + QM_TRY(MOZ_TO_RESULT(file->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + + mJournalsToRemoveAfterAbort.AppendElement(id); + } + + return NS_OK; +} + +nsresult DatabaseConnection::UpdateRefcountFunction::RemoveJournals( + const nsTArray<int64_t>& aJournals) { + MOZ_ASSERT(mConnection); + mConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL( + "DatabaseConnection::UpdateRefcountFunction::RemoveJournals", DOM); + + nsCOMPtr<nsIFile> journalDirectory = mFileManager.GetJournalDirectory(); + QM_TRY(OkIf(journalDirectory), NS_ERROR_FAILURE); + + for (const auto& journal : aJournals) { + nsCOMPtr<nsIFile> file = + DatabaseFileManager::GetFileForId(journalDirectory, journal); + QM_TRY(OkIf(file), NS_ERROR_FAILURE); + + QM_WARNONLY_TRY(QM_TO_RESULT(file->Remove(false))); + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(DatabaseConnection::UpdateRefcountFunction, + mozIStorageFunction) + +NS_IMETHODIMP +DatabaseConnection::UpdateRefcountFunction::OnFunctionCall( + mozIStorageValueArray* aValues, nsIVariant** _retval) { + MOZ_ASSERT(aValues); + MOZ_ASSERT(_retval); + + AUTO_PROFILER_LABEL( + "DatabaseConnection::UpdateRefcountFunction::OnFunctionCall", DOM); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const uint32_t& numEntries, + MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetNumEntries), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(numEntries == 2); + + QM_TRY_INSPECT(const int32_t& type1, + MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, 0), + QM_ASSERT_UNREACHABLE); + + QM_TRY_INSPECT(const int32_t& type2, + MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, 1), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(!(type1 == mozIStorageValueArray::VALUE_TYPE_NULL && + type2 == mozIStorageValueArray::VALUE_TYPE_NULL)); + } +#endif + + QM_TRY(MOZ_TO_RESULT(ProcessValue(aValues, 0, UpdateType::Decrement))); + + QM_TRY(MOZ_TO_RESULT(ProcessValue(aValues, 1, UpdateType::Increment))); + + return NS_OK; +} + +/******************************************************************************* + * ConnectionPool implementation + ******************************************************************************/ + +ConnectionPool::ConnectionPool() + : mDatabasesMutex("ConnectionPool::mDatabasesMutex"), + mIdleTimer(NS_NewTimer()), + mNextTransactionId(0), + mTotalThreadCount(0) { + 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); +} + +// static +void ConnectionPool::IdleTimerCallback(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(aTimer); + MOZ_ASSERT(aClosure); + + AUTO_PROFILER_LABEL("ConnectionPool::IdleTimerCallback", DOM); + + auto& self = *static_cast<ConnectionPool*>(aClosure); + 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. + const TimeStamp now = + TimeStamp::NowLoRes() + TimeDuration::FromMilliseconds(500); + + // XXX Move this to ArrayAlgorithm.h? + const auto removeUntil = [](auto& array, auto&& cond) { + const auto begin = array.begin(), end = array.end(); + array.RemoveElementsRange( + begin, std::find_if(begin, end, std::forward<decltype(cond)>(cond))); + }; + + removeUntil(self.mIdleDatabases, [now, &self](const auto& info) { + if (now >= info.mIdleTime) { + if ((*info.mDatabaseInfo)->mIdle) { + self.PerformIdleDatabaseMaintenance(*info.mDatabaseInfo.ref()); + } else { + self.CloseDatabase(*info.mDatabaseInfo.ref()); + } + + return false; + } + + return true; + }); + + removeUntil(self.mIdleThreads, [now, &self](auto& info) { + info.mThreadInfo.AssertValid(); + + if (now >= info.mIdleTime) { + self.ShutdownThread(std::move(info.mThreadInfo)); + + return false; + } + + return true; + }); + + self.AdjustIdleTimer(); +} + +Result<RefPtr<DatabaseConnection>, nsresult> +ConnectionPool::GetOrCreateConnection(const Database& aDatabase) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + AUTO_PROFILER_LABEL("ConnectionPool::GetOrCreateConnection", DOM); + + DatabaseInfo* dbInfo; + { + MutexAutoLock lock(mDatabasesMutex); + + dbInfo = mDatabases.Get(aDatabase.Id()); + } + + MOZ_ASSERT(dbInfo); + + if (dbInfo->mConnection) { + dbInfo->AssertIsOnConnectionThread(); + + return dbInfo->mConnection; + } + + MOZ_ASSERT(!dbInfo->mDEBUGConnectionThread); + + QM_TRY_UNWRAP( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> storageConnection, + GetStorageConnection(aDatabase.FilePath(), aDatabase.DirectoryLockId(), + aDatabase.TelemetryId(), aDatabase.MaybeKeyRef())); + + RefPtr<DatabaseConnection> connection = new DatabaseConnection( + std::move(storageConnection), aDatabase.GetFileManagerPtr()); + + QM_TRY(MOZ_TO_RESULT(connection->Init())); + + 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 + + return connection; +} + +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); + + AUTO_PROFILER_LABEL("ConnectionPool::Start", DOM); + + const uint64_t transactionId = ++mNextTransactionId; + + // To avoid always acquiring a lock, we don't use WithEntryHandle here, which + // would require a lock in any case. + DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId); + + const bool databaseInfoIsNew = !dbInfo; + + if (databaseInfoIsNew) { + MutexAutoLock lock(mDatabasesMutex); + + dbInfo = mDatabases + .InsertOrUpdate(aDatabaseId, + MakeUnique<DatabaseInfo>(this, aDatabaseId)) + .get(); + } + + MOZ_ASSERT(!mTransactions.Contains(transactionId)); + auto& transactionInfo = *mTransactions.InsertOrUpdate( + transactionId, MakeUnique<TransactionInfo>( + *dbInfo, aBackgroundChildLoggingId, aDatabaseId, + transactionId, aLoggingSerialNumber, aObjectStoreNames, + aIsWriteTransaction, aTransactionOp)); + + if (aIsWriteTransaction) { + MOZ_ASSERT(dbInfo->mWriteTransactionCount < UINT32_MAX); + dbInfo->mWriteTransactionCount++; + } else { + MOZ_ASSERT(dbInfo->mReadTransactionCount < UINT32_MAX); + dbInfo->mReadTransactionCount++; + } + + auto& blockingTransactions = dbInfo->mBlockingTransactions; + + for (const nsAString& objectStoreName : aObjectStoreNames) { + TransactionInfoPair* blockInfo = + blockingTransactions.GetOrInsertNew(objectStoreName); + + // Mark what we are blocking on. + if (const auto maybeBlockingRead = blockInfo->mLastBlockingReads) { + transactionInfo.mBlockedOn.Insert(&maybeBlockingRead.ref()); + maybeBlockingRead->AddBlockingTransaction(transactionInfo); + } + + if (aIsWriteTransaction) { + for (const auto blockingWrite : blockInfo->mLastBlockingWrites) { + transactionInfo.mBlockedOn.Insert(blockingWrite); + blockingWrite->AddBlockingTransaction(transactionInfo); + } + + blockInfo->mLastBlockingReads = SomeRef(transactionInfo); + blockInfo->mLastBlockingWrites.Clear(); + } else { + blockInfo->mLastBlockingWrites.AppendElement( + WrapNotNullUnchecked(&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); + + AUTO_PROFILER_LABEL("ConnectionPool::Dispatch", DOM); + + auto* const transactionInfo = mTransactions.Get(aTransactionId); + MOZ_ASSERT(transactionInfo); + MOZ_ASSERT(!transactionInfo->mFinished); + + if (transactionInfo->mRunning) { + DatabaseInfo& dbInfo = transactionInfo->mDatabaseInfo; + dbInfo.mThreadInfo.AssertValid(); + MOZ_ASSERT(!dbInfo.mClosing); + MOZ_ASSERT_IF( + transactionInfo->mIsWriteTransaction, + dbInfo.mRunningWriteTransaction && + dbInfo.mRunningWriteTransaction.refEquals(*transactionInfo)); + + MOZ_ALWAYS_SUCCEEDS( + dbInfo.mThreadInfo.ThreadRef().Dispatch(aRunnable, NS_DISPATCH_NORMAL)); + } else { + transactionInfo->mQueuedRunnables.AppendElement(aRunnable); + } +} + +void ConnectionPool::Finish(uint64_t aTransactionId, + FinishCallback* aCallback) { + AssertIsOnOwningThread(); + +#ifdef DEBUG + auto* const transactionInfo = mTransactions.Get(aTransactionId); + MOZ_ASSERT(transactionInfo); + MOZ_ASSERT(!transactionInfo->mFinished); +#endif + + AUTO_PROFILER_LABEL("ConnectionPool::Finish", DOM); + + RefPtr<FinishCallbackWrapper> wrapper = + new FinishCallbackWrapper(this, aTransactionId, aCallback); + + Dispatch(aTransactionId, wrapper); + +#ifdef DEBUG + transactionInfo->mFinished.Flip(); +#endif +} + +void ConnectionPool::WaitForDatabasesToComplete( + nsTArray<nsCString>&& aDatabaseIds, nsIRunnable* aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseIds.IsEmpty()); + MOZ_ASSERT(aCallback); + + AUTO_PROFILER_LABEL("ConnectionPool::WaitForDatabasesToComplete", DOM); + + bool mayRunCallbackImmediately = true; + + for (const nsACString& databaseId : aDatabaseIds) { + MOZ_ASSERT(!databaseId.IsEmpty()); + + if (CloseDatabaseWhenIdleInternal(databaseId)) { + mayRunCallbackImmediately = false; + } + } + + if (mayRunCallbackImmediately) { + Unused << aCallback->Run(); + return; + } + + mCompleteCallbacks.EmplaceBack(MakeUnique<DatabasesCompleteCallback>( + std::move(aDatabaseIds), aCallback)); +} + +void ConnectionPool::Shutdown() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mShutdownComplete); + + AUTO_PROFILER_LABEL("ConnectionPool::Shutdown", DOM); + + mShutdownRequested.Flip(); + + CancelIdleTimer(); + MOZ_ASSERT(mTargetIdleTime.IsNull()); + + mIdleTimer = nullptr; + + CloseIdleDatabases(); + + ShutdownIdleThreads(); + + if (!mDatabases.Count()) { + MOZ_ASSERT(!mTransactions.Count()); + + Cleanup(); + + MOZ_ASSERT(mShutdownComplete); + return; + } + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil("ConnectionPool::Shutdown"_ns, [&]() { + return static_cast<bool>(mShutdownComplete); + })); +} + +void ConnectionPool::Cleanup() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mShutdownRequested); + MOZ_ASSERT(!mShutdownComplete); + MOZ_ASSERT(!mDatabases.Count()); + MOZ_ASSERT(!mTransactions.Count()); + MOZ_ASSERT(mIdleThreads.IsEmpty()); + + AUTO_PROFILER_LABEL("ConnectionPool::Cleanup", DOM); + + if (!mCompleteCallbacks.IsEmpty()) { + // Run all callbacks manually now. + + { + auto completeCallbacks = std::move(mCompleteCallbacks); + for (const auto& completeCallback : completeCallbacks) { + MOZ_ASSERT(completeCallback); + MOZ_ASSERT(completeCallback->mCallback); + + Unused << completeCallback->mCallback->Run(); + } + + // We expect no new callbacks being completed by running the existing + // ones. + MOZ_ASSERT(mCompleteCallbacks.IsEmpty()); + } + + // And make sure they get processed. + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + MOZ_ALWAYS_SUCCEEDS(NS_ProcessPendingEvents(currentThread)); + } + + mShutdownComplete.Flip(); +} + +void ConnectionPool::AdjustIdleTimer() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mIdleTimer); + + AUTO_PROFILER_LABEL("ConnectionPool::AdjustIdleTimer", DOM); + + // 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->InitWithNamedFuncCallback( + IdleTimerCallback, this, delay, nsITimer::TYPE_ONE_SHOT, + "ConnectionPool::IdleTimerCallback")); + + 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(mTotalThreadCount); + + // We need to move thread and runnable separately. + auto [thread, runnable] = aThreadInfo.Forget(); + + IDB_DEBUG_LOG(("ConnectionPool shutting down thread %" PRIu32, + 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( + "nsIThread::AsyncShutdown", thread, &nsIThread::AsyncShutdown))); + + mTotalThreadCount--; +} + +void ConnectionPool::CloseIdleDatabases() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mShutdownRequested); + + AUTO_PROFILER_LABEL("ConnectionPool::CloseIdleDatabases", DOM); + + if (!mIdleDatabases.IsEmpty()) { + for (IdleDatabaseInfo& idleInfo : mIdleDatabases) { + CloseDatabase(*idleInfo.mDatabaseInfo.ref()); + } + mIdleDatabases.Clear(); + } + + if (!mDatabasesPerformingIdleMaintenance.IsEmpty()) { + for (const auto dbInfo : mDatabasesPerformingIdleMaintenance) { + CloseDatabase(*dbInfo); + } + mDatabasesPerformingIdleMaintenance.Clear(); + } +} + +void ConnectionPool::ShutdownIdleThreads() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mShutdownRequested); + + AUTO_PROFILER_LABEL("ConnectionPool::ShutdownIdleThreads", DOM); + + for (auto& idleThread : mIdleThreads) { + ShutdownThread(std::move(idleThread.mThreadInfo)); + } + mIdleThreads.Clear(); +} + +bool ConnectionPool::ScheduleTransaction(TransactionInfo& aTransactionInfo, + bool aFromQueuedTransactions) { + AssertIsOnOwningThread(); + + AUTO_PROFILER_LABEL("ConnectionPool::ScheduleTransaction", DOM); + + DatabaseInfo& dbInfo = aTransactionInfo.mDatabaseInfo; + + dbInfo.mIdle = false; + + if (dbInfo.mClosing) { + MOZ_ASSERT(!mIdleDatabases.Contains(&dbInfo)); + MOZ_ASSERT( + !dbInfo.mTransactionsScheduledDuringClose.Contains(&aTransactionInfo)); + + dbInfo.mTransactionsScheduledDuringClose.AppendElement( + WrapNotNullUnchecked(&aTransactionInfo)); + return true; + } + + if (!dbInfo.mThreadInfo.IsValid()) { + 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; + nsresult rv = NS_NewNamedThread(runnable->GetThreadName(), + getter_AddRefs(newThread), runnable); + if (NS_SUCCEEDED(rv)) { + newThread->SetNameForWakeupTelemetry("IndexedDB (all)"_ns); + MOZ_ASSERT(newThread); + + IDB_DEBUG_LOG(("ConnectionPool created thread %" PRIu32, + runnable->SerialNumber())); + + dbInfo.mThreadInfo = + ThreadInfo{std::move(newThread), std::move(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. + // + // This is copied for each database inside the loop below, it is + // deliberately const to prevent the attempt to wrongly optimize the + // refcounting by passing runnable.forget() to the Dispatch method, see + // bug 1598559. + const nsCOMPtr<nsIRunnable> runnable = + new Runnable("IndexedDBDummyRunnable"); + + for (uint32_t index = mDatabasesPerformingIdleMaintenance.Length(); + index > 0; index--) { + const auto dbInfo = mDatabasesPerformingIdleMaintenance[index - 1]; + dbInfo->mThreadInfo.AssertValid(); + + MOZ_ALWAYS_SUCCEEDS(dbInfo->mThreadInfo.ThreadRef().Dispatch( + runnable, NS_DISPATCH_NORMAL)); + } + } + + if (!created) { + if (!aFromQueuedTransactions) { + MOZ_ASSERT(!mQueuedTransactions.Contains(&aTransactionInfo)); + mQueuedTransactions.AppendElement( + WrapNotNullUnchecked(&aTransactionInfo)); + } + return false; + } + } else { + dbInfo.mThreadInfo = std::move(mIdleThreads.PopLastElement().mThreadInfo); + + AdjustIdleTimer(); + } + } + + dbInfo.mThreadInfo.AssertValid(); + + 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( + WrapNotNullUnchecked(&aTransactionInfo)); + return true; + } + + dbInfo.mRunningWriteTransaction = SomeRef(aTransactionInfo); + dbInfo.mNeedsCheckpoint = true; + } + + MOZ_ASSERT(!aTransactionInfo.mRunning); + aTransactionInfo.mRunning = true; + + nsTArray<nsCOMPtr<nsIRunnable>>& queuedRunnables = + aTransactionInfo.mQueuedRunnables; + + if (!queuedRunnables.IsEmpty()) { + for (auto& queuedRunnable : queuedRunnables) { + MOZ_ALWAYS_SUCCEEDS(dbInfo.mThreadInfo.ThreadRef().Dispatch( + queuedRunnable.forget(), NS_DISPATCH_NORMAL)); + } + + queuedRunnables.Clear(); + } + + return true; +} + +void ConnectionPool::NoteFinishedTransaction(uint64_t aTransactionId) { + AssertIsOnOwningThread(); + + AUTO_PROFILER_LABEL("ConnectionPool::NoteFinishedTransaction", DOM); + + auto* const transactionInfo = mTransactions.Get(aTransactionId); + MOZ_ASSERT(transactionInfo); + MOZ_ASSERT(transactionInfo->mRunning); + MOZ_ASSERT(transactionInfo->mFinished); + + transactionInfo->mRunning = false; + + DatabaseInfo& dbInfo = transactionInfo->mDatabaseInfo; + MOZ_ASSERT(mDatabases.Get(transactionInfo->mDatabaseId) == &dbInfo); + dbInfo.mThreadInfo.AssertValid(); + + // Schedule the next write transaction if there are any queued. + if (dbInfo.mRunningWriteTransaction && + dbInfo.mRunningWriteTransaction.refEquals(*transactionInfo)) { + MOZ_ASSERT(transactionInfo->mIsWriteTransaction); + MOZ_ASSERT(dbInfo.mNeedsCheckpoint); + + dbInfo.mRunningWriteTransaction = Nothing(); + + if (!dbInfo.mScheduledWriteTransactions.IsEmpty()) { + const auto nextWriteTransaction = dbInfo.mScheduledWriteTransactions[0]; + + dbInfo.mScheduledWriteTransactions.RemoveElementAt(0); + + MOZ_ALWAYS_TRUE(ScheduleTransaction(*nextWriteTransaction, + /* aFromQueuedTransactions */ false)); + } + } + + for (const auto& objectStoreName : transactionInfo->mObjectStoreNames) { + TransactionInfoPair* blockInfo = + dbInfo.mBlockingTransactions.Get(objectStoreName); + MOZ_ASSERT(blockInfo); + + if (transactionInfo->mIsWriteTransaction && blockInfo->mLastBlockingReads && + blockInfo->mLastBlockingReads.refEquals(*transactionInfo)) { + blockInfo->mLastBlockingReads = Nothing(); + } + + 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); + + if (!dbInfo.TotalTransactionCount()) { + MOZ_ASSERT(!dbInfo.mIdle); + dbInfo.mIdle = true; + + NoteIdleDatabase(dbInfo); + } +} + +void ConnectionPool::ScheduleQueuedTransactions(ThreadInfo aThreadInfo) { + AssertIsOnOwningThread(); + aThreadInfo.AssertValid(); + MOZ_ASSERT(!mQueuedTransactions.IsEmpty()); + + AUTO_PROFILER_LABEL("ConnectionPool::ScheduleQueuedTransactions", DOM); + + auto idleThreadInfo = IdleThreadInfo{std::move(aThreadInfo)}; + MOZ_ASSERT(!mIdleThreads.Contains(idleThreadInfo)); + mIdleThreads.InsertElementSorted(std::move(idleThreadInfo)); + + const auto foundIt = std::find_if( + mQueuedTransactions.begin(), mQueuedTransactions.end(), + [&me = *this](const auto& queuedTransaction) { + return !me.ScheduleTransaction(*queuedTransaction, + /* aFromQueuedTransactions */ true); + }); + + mQueuedTransactions.RemoveElementsRange(mQueuedTransactions.begin(), foundIt); + + AdjustIdleTimer(); +} + +void ConnectionPool::NoteIdleDatabase(DatabaseInfo& aDatabaseInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseInfo.TotalTransactionCount()); + aDatabaseInfo.mThreadInfo.AssertValid(); + MOZ_ASSERT(!mIdleDatabases.Contains(&aDatabaseInfo)); + + AUTO_PROFILER_LABEL("ConnectionPool::NoteIdleDatabase", DOM); + + 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(std::move(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(std::move(aDatabaseInfo.mThreadInfo)); + } + + return; + } + + mIdleDatabases.InsertElementSorted(IdleDatabaseInfo{aDatabaseInfo}); + + AdjustIdleTimer(); +} + +void ConnectionPool::NoteClosedDatabase(DatabaseInfo& aDatabaseInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabaseInfo.mClosing); + MOZ_ASSERT(!mIdleDatabases.Contains(&aDatabaseInfo)); + + AUTO_PROFILER_LABEL("ConnectionPool::NoteClosedDatabase", DOM); + + 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.IsValid()) { + if (!mQueuedTransactions.IsEmpty()) { + // Give the thread to another database. + ScheduleQueuedTransactions(std::move(aDatabaseInfo.mThreadInfo)); + } else if (!aDatabaseInfo.TotalTransactionCount()) { + if (mShutdownRequested) { + ShutdownThread(std::move(aDatabaseInfo.mThreadInfo)); + } else { + auto idleThreadInfo = + IdleThreadInfo{std::move(aDatabaseInfo.mThreadInfo)}; + MOZ_ASSERT(!mIdleThreads.Contains(idleThreadInfo)); + + mIdleThreads.InsertElementSorted(std::move(idleThreadInfo)); + + if (mIdleThreads.Length() > kMaxIdleConnectionThreadCount) { + ShutdownThread(std::move(mIdleThreads[0].mThreadInfo)); + mIdleThreads.RemoveElementAt(0); + } + + AdjustIdleTimer(); + } + } + } + + // Schedule any transactions that were started while we were closing the + // connection. + if (aDatabaseInfo.TotalTransactionCount()) { + auto& scheduledTransactions = + aDatabaseInfo.mTransactionsScheduledDuringClose; + + MOZ_ASSERT(!scheduledTransactions.IsEmpty()); + + for (const auto& scheduledTransaction : scheduledTransactions) { + Unused << ScheduleTransaction(*scheduledTransaction, + /* 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); + } + + // That just deleted |aDatabaseInfo|, we must not access that below. + + // See if we need to fire any complete callbacks now that the database is + // finished. + mCompleteCallbacks.RemoveLastElements( + mCompleteCallbacks.end() - + std::remove_if(mCompleteCallbacks.begin(), mCompleteCallbacks.end(), + [&me = *this](const auto& completeCallback) { + return me.MaybeFireCallback(completeCallback.get()); + })); + + // 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); + + AUTO_PROFILER_LABEL("ConnectionPool::MaybeFireCallback", DOM); + + if (std::any_of(aCallback->mDatabaseIds.begin(), + aCallback->mDatabaseIds.end(), + [&databases = mDatabases](const auto& databaseId) { + MOZ_ASSERT(!databaseId.IsEmpty()); + + return databases.Get(databaseId); + })) { + return false; + } + + Unused << aCallback->mCallback->Run(); + return true; +} + +void ConnectionPool::PerformIdleDatabaseMaintenance( + DatabaseInfo& aDatabaseInfo) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseInfo.TotalTransactionCount()); + aDatabaseInfo.mThreadInfo.AssertValid(); + MOZ_ASSERT(aDatabaseInfo.mIdle); + MOZ_ASSERT(!aDatabaseInfo.mCloseOnIdle); + MOZ_ASSERT(!aDatabaseInfo.mClosing); + MOZ_ASSERT(mIdleDatabases.Contains(&aDatabaseInfo)); + MOZ_ASSERT(!mDatabasesPerformingIdleMaintenance.Contains(&aDatabaseInfo)); + + const bool neededCheckpoint = aDatabaseInfo.mNeedsCheckpoint; + + aDatabaseInfo.mNeedsCheckpoint = false; + aDatabaseInfo.mIdle = false; + + mDatabasesPerformingIdleMaintenance.AppendElement( + WrapNotNullUnchecked(&aDatabaseInfo)); + + MOZ_ALWAYS_SUCCEEDS(aDatabaseInfo.mThreadInfo.ThreadRef().Dispatch( + MakeAndAddRef<IdleConnectionRunnable>(aDatabaseInfo, neededCheckpoint), + NS_DISPATCH_NORMAL)); +} + +void ConnectionPool::CloseDatabase(DatabaseInfo& aDatabaseInfo) const { + AssertIsOnOwningThread(); + MOZ_DIAGNOSTIC_ASSERT(!aDatabaseInfo.TotalTransactionCount()); + aDatabaseInfo.mThreadInfo.AssertValid(); + MOZ_ASSERT(!aDatabaseInfo.mClosing); + + aDatabaseInfo.mIdle = false; + aDatabaseInfo.mNeedsCheckpoint = false; + aDatabaseInfo.mClosing = true; + + MOZ_ALWAYS_SUCCEEDS(aDatabaseInfo.mThreadInfo.ThreadRef().Dispatch( + MakeAndAddRef<CloseConnectionRunnable>(aDatabaseInfo), + NS_DISPATCH_NORMAL)); +} + +bool ConnectionPool::CloseDatabaseWhenIdleInternal( + const nsACString& aDatabaseId) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aDatabaseId.IsEmpty()); + + AUTO_PROFILER_LABEL("ConnectionPool::CloseDatabaseWhenIdleInternal", DOM); + + if (DatabaseInfo* dbInfo = mDatabases.Get(aDatabaseId)) { + if (mIdleDatabases.RemoveElement(dbInfo) || + mDatabasesPerformingIdleMaintenance.RemoveElement(dbInfo)) { + CloseDatabase(*dbInfo); + AdjustIdleTimer(); + } else { + dbInfo->mCloseOnIdle.EnsureFlipped(); + } + + return true; + } + + return false; +} + +ConnectionPool::ConnectionRunnable::ConnectionRunnable( + DatabaseInfo& aDatabaseInfo) + : Runnable("dom::indexedDB::ConnectionPool::ConnectionRunnable"), + mDatabaseInfo(aDatabaseInfo), + mOwningEventTarget(GetCurrentSerialEventTarget()) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseInfo.mConnectionPool); + aDatabaseInfo.mConnectionPool->AssertIsOnOwningThread(); + MOZ_ASSERT(mOwningEventTarget); +} + +NS_IMETHODIMP +ConnectionPool::IdleConnectionRunnable::Run() { + MOZ_ASSERT(!mDatabaseInfo.mIdle); + + const nsCOMPtr<nsIEventTarget> owningThread = std::move(mOwningEventTarget); + + 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; + } + + AssertIsOnBackgroundThread(); + + 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_IMETHODIMP +ConnectionPool::CloseConnectionRunnable::Run() { + AUTO_PROFILER_LABEL("ConnectionPool::CloseConnectionRunnable::Run", DOM); + + if (mOwningEventTarget) { + MOZ_ASSERT(mDatabaseInfo.mClosing); + + const nsCOMPtr<nsIEventTarget> owningThread = std::move(mOwningEventTarget); + + // 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), + mReadTransactionCount(0), + mWriteTransactionCount(0), + mNeedsCheckpoint(false), + mIdle(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); + mThreadInfo.AssertEmpty(); + MOZ_ASSERT(!TotalTransactionCount()); + + MOZ_COUNT_DTOR(ConnectionPool::DatabaseInfo); +} + +ConnectionPool::DatabasesCompleteCallback::DatabasesCompleteCallback( + nsTArray<nsCString>&& aDatabaseIds, nsIRunnable* aCallback) + : mDatabaseIds(std::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) + : Runnable("dom::indexedDB::ConnectionPool::FinishCallbackWrapper"), + mConnectionPool(aConnectionPool), + mCallback(aCallback), + mOwningEventTarget(GetCurrentSerialEventTarget()), + mTransactionId(aTransactionId), + mHasRunOnce(false) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aConnectionPool); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(mOwningEventTarget); +} + +ConnectionPool::FinishCallbackWrapper::~FinishCallbackWrapper() { + MOZ_ASSERT(!mConnectionPool); + MOZ_ASSERT(!mCallback); +} + +nsresult ConnectionPool::FinishCallbackWrapper::Run() { + MOZ_ASSERT(mConnectionPool); + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mOwningEventTarget); + + AUTO_PROFILER_LABEL("ConnectionPool::FinishCallbackWrapper::Run", DOM); + + if (!mHasRunOnce) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + mHasRunOnce = true; + + Unused << mCallback->Run(); + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; + } + + mConnectionPool->AssertIsOnOwningThread(); + MOZ_ASSERT(mHasRunOnce); + + RefPtr<ConnectionPool> connectionPool = std::move(mConnectionPool); + RefPtr<FinishCallback> callback = std::move(mCallback); + + callback->TransactionFinishedBeforeUnblock(); + + connectionPool->NoteFinishedTransaction(mTransactionId); + + callback->TransactionFinishedAfterUnblock(); + + return NS_OK; +} + +uint32_t ConnectionPool::ThreadRunnable::sNextSerialNumber = 0; + +ConnectionPool::ThreadRunnable::ThreadRunnable() + : Runnable("dom::indexedDB::ConnectionPool::ThreadRunnable"), + mSerialNumber(++sNextSerialNumber) { + AssertIsOnBackgroundThread(); +} + +ConnectionPool::ThreadRunnable::~ThreadRunnable() { + MOZ_ASSERT(!mFirstRun); + MOZ_ASSERT(!mContinueRunning); +} + +nsresult ConnectionPool::ThreadRunnable::Run() { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mContinueRunning); + + if (!mFirstRun) { + mContinueRunning.Flip(); + return NS_OK; + } + + mFirstRun.Flip(); + + { + // Scope for the profiler label. + AUTO_PROFILER_LABEL("ConnectionPool::ThreadRunnable::Run", DOM); + + DebugOnly<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 + + DebugOnly<bool> b = + SpinEventLoopUntil("ConnectionPool::ThreadRunnable"_ns, [&]() -> bool { + if (!mContinueRunning) { + return true; + } + +#ifdef DEBUG + if (kDEBUGTransactionThreadSleepMS) { + MOZ_ALWAYS_TRUE(PR_Sleep(PR_MillisecondsToInterval( + kDEBUGTransactionThreadSleepMS)) == PR_SUCCESS); + } +#endif // DEBUG + + return false; + }); + // MSVC can't stringify lambdas, so we have to separate the expression + // generating the value from the assert itself. +#if DEBUG + MOZ_ALWAYS_TRUE(b); +#endif + } + + return NS_OK; +} + +ConnectionPool::ThreadInfo::ThreadInfo() { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo); +} + +ConnectionPool::ThreadInfo::ThreadInfo(ThreadInfo&& aOther) noexcept + : mThread(std::move(aOther.mThread)), + mRunnable(std::move(aOther.mRunnable)) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mThread); + MOZ_ASSERT(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(WrapNotNullUnchecked(&aDatabaseInfo)) { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo); +} + +ConnectionPool::IdleDatabaseInfo::~IdleDatabaseInfo() { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::IdleDatabaseInfo); +} + +ConnectionPool::IdleThreadInfo::IdleThreadInfo(ThreadInfo aThreadInfo) + : IdleResource(TimeStamp::NowLoRes() + + TimeDuration::FromMilliseconds(kConnectionThreadIdleMS)), + mThreadInfo(std::move(aThreadInfo)) { + AssertIsOnBackgroundThread(); + mThreadInfo.AssertValid(); + + 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.Clone()), + mIsWriteTransaction(aIsWriteTransaction), + mRunning(false) { + AssertIsOnBackgroundThread(); + 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(); + + // XXX Does it really make sense to have both mBlocking and mBlockingOrdered, + // just to reduce the algorithmic complexity of this Contains check? This was + // mentioned in the context of Bug 1290853, but no real justification was + // given. There was the suggestion of encapsulating this in an + // insertion-ordered hashtable implementation, which seems like a good idea. + // If we had that, this would be the appropriate data structure to use here. + if (mBlocking.EnsureInserted(&aTransactionInfo)) { + mBlockingOrdered.AppendElement(WrapNotNullUnchecked(&aTransactionInfo)); + } +} + +void ConnectionPool::TransactionInfo::RemoveBlockingTransactions() { + AssertIsOnBackgroundThread(); + + for (const auto blockedInfo : mBlockingOrdered) { + blockedInfo->MaybeUnblock(*this); + } + + mBlocking.Clear(); + mBlockingOrdered.Clear(); +} + +void ConnectionPool::TransactionInfo::MaybeUnblock( + TransactionInfo& aTransactionInfo) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mBlockedOn.Contains(&aTransactionInfo)); + + mBlockedOn.Remove(&aTransactionInfo); + if (mBlockedOn.IsEmpty()) { + ConnectionPool* connectionPool = mDatabaseInfo.mConnectionPool; + MOZ_ASSERT(connectionPool); + connectionPool->AssertIsOnOwningThread(); + + Unused << connectionPool->ScheduleTransaction( + *this, + /* aFromQueuedTransactions */ false); + } +} + +#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) +ConnectionPool::TransactionInfoPair::TransactionInfoPair() { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_CTOR(ConnectionPool::TransactionInfoPair); +} + +ConnectionPool::TransactionInfoPair::~TransactionInfoPair() { + AssertIsOnBackgroundThread(); + + MOZ_COUNT_DTOR(ConnectionPool::TransactionInfoPair); +} +#endif + +/******************************************************************************* + * Metadata classes + ******************************************************************************/ + +bool FullObjectStoreMetadata::HasLiveIndexes() const { + AssertIsOnBackgroundThread(); + + return std::any_of(mIndexes.Values().cbegin(), mIndexes.Values().cend(), + [](const auto& entry) { return !entry->mDeleted; }); +} + +SafeRefPtr<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. + auto newMetadata = MakeSafeRefPtr<FullDatabaseMetadata>(mCommonMetadata); + + newMetadata->mDatabaseId = mDatabaseId; + newMetadata->mFilePath = mFilePath; + newMetadata->mNextObjectStoreId = mNextObjectStoreId; + newMetadata->mNextIndexId = mNextIndexId; + + for (const auto& objectStoreEntry : mObjectStores) { + const auto& objectStoreValue = objectStoreEntry.GetData(); + + auto newOSMetadata = MakeSafeRefPtr<FullObjectStoreMetadata>( + objectStoreValue->mCommonMetadata, [&objectStoreValue] { + const auto&& srcLocked = objectStoreValue->mAutoIncrementIds.Lock(); + return *srcLocked; + }()); + + for (const auto& indexEntry : objectStoreValue->mIndexes) { + const auto& value = indexEntry.GetData(); + + auto newIndexMetadata = MakeSafeRefPtr<FullIndexMetadata>(); + + newIndexMetadata->mCommonMetadata = value->mCommonMetadata; + + if (NS_WARN_IF(!newOSMetadata->mIndexes.InsertOrUpdate( + indexEntry.GetKey(), std::move(newIndexMetadata), fallible))) { + return nullptr; + } + } + + MOZ_ASSERT(objectStoreValue->mIndexes.Count() == + newOSMetadata->mIndexes.Count()); + + if (NS_WARN_IF(!newMetadata->mObjectStores.InsertOrUpdate( + objectStoreEntry.GetKey(), std::move(newOSMetadata), fallible))) { + return nullptr; + } + } + + MOZ_ASSERT(mObjectStores.Count() == newMetadata->mObjectStores.Count()); + + return newMetadata; +} + +DatabaseLoggingInfo::~DatabaseLoggingInfo() { + AssertIsOnBackgroundThread(); + + if (gLoggingInfoHashtable) { + const nsID& backgroundChildLoggingId = + mLoggingInfo.backgroundChildLoggingId(); + + MOZ_ASSERT(gLoggingInfoHashtable->Get(backgroundChildLoggingId) == this); + + gLoggingInfoHashtable->Remove(backgroundChildLoggingId); + } +} + +/******************************************************************************* + * Factory + ******************************************************************************/ + +Factory::Factory(RefPtr<DatabaseLoggingInfo> aLoggingInfo) + : mLoggingInfo(std::move(aLoggingInfo)) +#ifdef DEBUG + , + mActorDestroyed(false) +#endif +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); +} + +Factory::~Factory() { MOZ_ASSERT(mActorDestroyed); } + +// static +SafeRefPtr<Factory> Factory::Create(const LoggingInfo& aLoggingInfo) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + + // Balanced in ActoryDestroy(). + IncreaseBusyCount(); + + MOZ_ASSERT(gLoggingInfoHashtable); + RefPtr<DatabaseLoggingInfo> loggingInfo = + gLoggingInfoHashtable->WithEntryHandle( + aLoggingInfo.backgroundChildLoggingId(), [&](auto&& entry) { + if (entry) { + [[maybe_unused]] const auto& loggingInfo = entry.Data(); + MOZ_ASSERT(aLoggingInfo.backgroundChildLoggingId() == + loggingInfo->Id()); +#if !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 // !FUZZING + } else { + entry.Insert(new DatabaseLoggingInfo(aLoggingInfo)); + } + + return do_AddRef(entry.Data()); + }); + + return MakeSafeRefPtr<Factory>(std::move(loggingInfo)); +} + +void Factory::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + +#ifdef DEBUG + mActorDestroyed = true; +#endif + + // Match the IncreaseBusyCount in Create(). + DecreaseBusyCount(); +} + +mozilla::ipc::IPCResult Factory::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + QM_WARNONLY_TRY(OkIf(PBackgroundIDBFactoryParent::Send__delete__(this))); + + return IPC_OK(); +} + +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_AUUF_OR_WARN_IF(!IsValidPersistenceType(metadata.persistenceType()))) { + return nullptr; + } + + const PrincipalInfo& principalInfo = commonParams->principalInfo(); + if (NS_AUUF_OR_WARN_IF(principalInfo.type() == + PrincipalInfo::TNullPrincipalInfo)) { + return nullptr; + } + + if (NS_AUUF_OR_WARN_IF( + principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && + metadata.persistenceType() != PERSISTENCE_TYPE_PERSISTENT)) { + return nullptr; + } + + if (NS_AUUF_OR_WARN_IF(!QuotaManager::IsPrincipalInfoValid(principalInfo))) { + return nullptr; + } + + RefPtr<ThreadsafeContentParentHandle> contentHandle = + BackgroundParent::GetContentParentHandle(Manager()); + + auto actor = [&]() -> RefPtr<FactoryOp> { + if (aParams.type() == FactoryRequestParams::TOpenDatabaseRequestParams) { + return MakeRefPtr<OpenDatabaseOp>( + SafeRefPtrFromThis(), std::move(contentHandle), *commonParams); + } else { + return MakeRefPtr<DeleteDatabaseOp>( + SafeRefPtrFromThis(), std::move(contentHandle), *commonParams); + } + }(); + + gFactoryOps->AppendElement(actor); + + // Balanced in CleanupMetadata() which is/must always called by SendResults(). + IncreaseBusyCount(); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult 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 IPC_OK(); +} + +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, + NotNull<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) { + mState = State::WaitingForTransactions; + + connectionPool->WaitForDatabasesToComplete(nsTArray<nsCString>{mDatabaseId}, + this); + return; + } + + CallCallback(); +} + +void WaitForTransactionsHelper::CallCallback() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial || + mState == State::WaitingForTransactions); + + const nsCOMPtr<nsIRunnable> callback = std::move(mCallback); + + callback->Run(); + + mState = State::Complete; +} + +NS_IMETHODIMP +WaitForTransactionsHelper::Run() { + MOZ_ASSERT(mState != State::Complete); + MOZ_ASSERT(mCallback); + + switch (mState) { + case State::Initial: + MaybeWaitForTransactions(); + break; + + case State::WaitingForTransactions: + CallCallback(); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + return NS_OK; +} + +/******************************************************************************* + * Database + ******************************************************************************/ + +Database::Database(SafeRefPtr<Factory> aFactory, + const PrincipalInfo& aPrincipalInfo, + const Maybe<ContentParentId>& aOptionalContentParentId, + const quota::OriginMetadata& aOriginMetadata, + uint32_t aTelemetryId, + SafeRefPtr<FullDatabaseMetadata> aMetadata, + SafeRefPtr<DatabaseFileManager> aFileManager, + RefPtr<DirectoryLock> aDirectoryLock, + bool aChromeWriteAccessAllowed, bool aInPrivateBrowsing, + const Maybe<const CipherKey>& aMaybeKey) + : mFactory(std::move(aFactory)), + mMetadata(std::move(aMetadata)), + mFileManager(std::move(aFileManager)), + mDirectoryLock(std::move(aDirectoryLock)), + mPrincipalInfo(aPrincipalInfo), + mOptionalContentParentId(aOptionalContentParentId), + mOriginMetadata(aOriginMetadata), + mId(mMetadata->mDatabaseId), + mFilePath(mMetadata->mFilePath), + mKey(aMaybeKey), + mTelemetryId(aTelemetryId), + mPersistenceType(mMetadata->mCommonMetadata.persistenceType()), + mChromeWriteAccessAllowed(aChromeWriteAccessAllowed), + mInPrivateBrowsing(aInPrivateBrowsing), + mBackgroundThread(GetCurrentSerialEventTarget()) +#ifdef DEBUG + , + mAllBlobsUnmapped(false) +#endif +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFactory); + MOZ_ASSERT(mMetadata); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT_IF(aChromeWriteAccessAllowed, + aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo); + + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(mDirectoryLock->Id() >= 0); + mDirectoryLockId = mDirectoryLock->Id(); +} + +template <typename T> +bool Database::InvalidateAll(const nsTBaseHashSet<nsPtrHashKey<T>>& aTable) { + AssertIsOnBackgroundThread(); + + const uint32_t count = aTable.Count(); + if (!count) { + return true; + } + + // XXX Does this really need to be fallible? + QM_TRY_INSPECT(const auto& elementsToInvalidate, + TransformIntoNewArray( + aTable, [](const auto& entry) { return entry; }, fallible), + false); + + IDB_REPORT_INTERNAL_ERR(); + + for (const auto& elementToInvalidate : elementsToInvalidate) { + MOZ_ASSERT(elementToInvalidate); + + elementToInvalidate->Invalidate(); + } + + return true; +} + +void Database::Invalidate() { + AssertIsOnBackgroundThread(); + + if (mInvalidated) { + return; + } + + mInvalidated.Flip(); + + if (mActorWasAlive && !mActorDestroyed) { + Unused << SendInvalidate(); + } + + QM_WARNONLY_TRY(OkIf(InvalidateAll(mTransactions))); + + MOZ_ALWAYS_TRUE(CloseInternal()); +} + +nsresult Database::EnsureConnection() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + AUTO_PROFILER_LABEL("Database::EnsureConnection", DOM); + + if (!mConnection || !mConnection->HasStorageConnection()) { + QM_TRY_UNWRAP(mConnection, gConnectionPool->GetOrCreateConnection(*this)); + } + + AssertIsOnConnectionThread(); + + return NS_OK; +} + +bool Database::RegisterTransaction(TransactionBase& aTransaction) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mTransactions.Contains(&aTransaction)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mInvalidated); + MOZ_ASSERT(!mClosed); + + if (NS_WARN_IF(!mTransactions.Insert(&aTransaction, fallible))) { + return false; + } + + return true; +} + +void Database::UnregisterTransaction(TransactionBase& aTransaction) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransactions.Contains(&aTransaction)); + + mTransactions.Remove(&aTransaction); + + MaybeCloseConnection(); +} + +void Database::SetActorAlive() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + mActorWasAlive.Flip(); + + // This reference will be absorbed by IPDL and released when the actor is + // destroyed. + AddRef(); +} + +void Database::MapBlob(const IPCBlob& aIPCBlob, + SafeRefPtr<DatabaseFileInfo> aFileInfo) { + AssertIsOnBackgroundThread(); + + const RemoteLazyStream& stream = aIPCBlob.inputStream(); + MOZ_ASSERT(stream.type() == RemoteLazyStream::TRemoteLazyInputStream); + + nsID id{}; + MOZ_ALWAYS_SUCCEEDS( + stream.get_RemoteLazyInputStream()->GetInternalStreamID(id)); + + MOZ_ASSERT(!mMappedBlobs.Contains(id)); + mMappedBlobs.InsertOrUpdate(id, std::move(aFileInfo)); + + RefPtr<UnmapBlobCallback> callback = + new UnmapBlobCallback(SafeRefPtrFromThis()); + + auto storage = RemoteLazyInputStreamStorage::Get(); + MOZ_ASSERT(storage.isOk()); + storage.inspect()->StoreCallback(id, callback); +} + +void Database::Stringify(nsACString& aResult) const { + AssertIsOnBackgroundThread(); + + constexpr auto kQuotaGenericDelimiterString = "|"_ns; + + aResult.Append( + "DirectoryLock:"_ns + IntToCString(!!mDirectoryLock) + + kQuotaGenericDelimiterString + + // + "Transactions:"_ns + IntToCString(mTransactions.Count()) + + kQuotaGenericDelimiterString + + // + "OtherProcessActor:"_ns + + IntToCString( + BackgroundParent::IsOtherProcessActor(GetBackgroundParent())) + + kQuotaGenericDelimiterString + + // + "Origin:"_ns + AnonymizedOriginString(mOriginMetadata.mOrigin) + + kQuotaGenericDelimiterString + + // + "PersistenceType:"_ns + PersistenceTypeToString(mPersistenceType) + + kQuotaGenericDelimiterString + + // + "Closed:"_ns + IntToCString(static_cast<bool>(mClosed)) + + kQuotaGenericDelimiterString + + // + "Invalidated:"_ns + IntToCString(static_cast<bool>(mInvalidated)) + + kQuotaGenericDelimiterString + + // + "ActorWasAlive:"_ns + IntToCString(static_cast<bool>(mActorWasAlive)) + + kQuotaGenericDelimiterString + + // + "ActorDestroyed:"_ns + IntToCString(static_cast<bool>(mActorDestroyed))); +} + +SafeRefPtr<DatabaseFileInfo> Database::GetBlob(const IPCBlob& aIPCBlob) { + AssertIsOnBackgroundThread(); + + RefPtr<RemoteLazyInputStream> lazyStream; + switch (aIPCBlob.inputStream().type()) { + case RemoteLazyStream::TIPCStream: { + const InputStreamParams& inputStreamParams = + aIPCBlob.inputStream().get_IPCStream().stream(); + if (inputStreamParams.type() != + InputStreamParams::TRemoteLazyInputStreamParams) { + return nullptr; + } + lazyStream = inputStreamParams.get_RemoteLazyInputStreamParams().stream(); + break; + } + case RemoteLazyStream::TRemoteLazyInputStream: + lazyStream = aIPCBlob.inputStream().get_RemoteLazyInputStream(); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown RemoteLazyStream type"); + return nullptr; + } + + if (!lazyStream) { + MOZ_ASSERT_UNREACHABLE("Unexpected null stream"); + return nullptr; + } + + nsID id{}; + nsresult rv = lazyStream->GetInternalStreamID(id); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE( + "Received RemoteLazyInputStream doesn't have an actor connection"); + return nullptr; + } + + const auto fileInfo = mMappedBlobs.Lookup(id); + return fileInfo ? fileInfo->clonePtr() : nullptr; +} + +void Database::UnmapBlob(const nsID& aID) { + AssertIsOnBackgroundThread(); + + MOZ_ASSERT_IF(!mAllBlobsUnmapped, mMappedBlobs.Contains(aID)); + mMappedBlobs.Remove(aID); +} + +void Database::UnmapAllBlobs() { + AssertIsOnBackgroundThread(); + +#ifdef DEBUG + mAllBlobsUnmapped = true; +#endif + + mMappedBlobs.Clear(); +} + +bool Database::CloseInternal() { + AssertIsOnBackgroundThread(); + + if (mClosed) { + if (NS_WARN_IF(!IsInvalidated())) { + // Signal misbehaving child for sending the close message twice. + return false; + } + + // Ignore harmless race when we just invalidated the database. + return true; + } + + mClosed.Flip(); + + 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() && IsClosed() && mDirectoryLock) { + nsCOMPtr<nsIRunnable> callback = + NewRunnableMethod("dom::indexedDB::Database::ConnectionClosedCallback", + this, &Database::ConnectionClosedCallback); + + RefPtr<WaitForTransactionsHelper> helper = + new WaitForTransactionsHelper(Id(), callback); + helper->WaitForTransactions(); + } +} + +void Database::ConnectionClosedCallback() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mClosed); + MOZ_ASSERT(!mTransactions.Count()); + + mDirectoryLock = nullptr; + + CleanupMetadata(); + + UnmapAllBlobs(); + + 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(); + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); + MOZ_ALWAYS_TRUE(info->mLiveDatabases.RemoveElement(this)); + + QuotaManager::MaybeRecordQuotaClientShutdownStep( + quota::Client::IDB, "Live database entry removed"_ns); + + if (info->mLiveDatabases.IsEmpty()) { + MOZ_ASSERT(!info->mWaitingFactoryOp || + !info->mWaitingFactoryOp->HasBlockedDatabases()); + gLiveDatabaseHashtable->Remove(Id()); + + QuotaManager::MaybeRecordQuotaClientShutdownStep( + quota::Client::IDB, "gLiveDatabaseHashtable entry removed"_ns); + } + + // Match the IncreaseBusyCount in OpenDatabaseOp::EnsureDatabaseActor(). + DecreaseBusyCount(); +} + +void Database::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + + mActorDestroyed.Flip(); + + if (!IsInvalidated()) { + Invalidate(); + } +} + +PBackgroundIDBDatabaseFileParent* +Database::AllocPBackgroundIDBDatabaseFileParent(const IPCBlob& aIPCBlob) { + AssertIsOnBackgroundThread(); + + SafeRefPtr<DatabaseFileInfo> fileInfo = GetBlob(aIPCBlob); + RefPtr<DatabaseFile> actor; + + if (fileInfo) { + actor = new DatabaseFile(std::move(fileInfo)); + } else { + // This is a blob we haven't seen before. + fileInfo = mFileManager->CreateFileInfo(); + if (NS_WARN_IF(!fileInfo)) { + return nullptr; + } + + actor = new DatabaseFile(IPCBlobUtils::Deserialize(aIPCBlob), + std::move(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; +} + +already_AddRefed<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)) { + MOZ_ASSERT_UNLESS_FUZZING(mInvalidated); + return nullptr; + } + + if (NS_AUUF_OR_WARN_IF(aObjectStoreNames.IsEmpty())) { + return nullptr; + } + + if (NS_AUUF_OR_WARN_IF(aMode != IDBTransaction::Mode::ReadOnly && + aMode != IDBTransaction::Mode::ReadWrite && + aMode != IDBTransaction::Mode::ReadWriteFlush && + aMode != IDBTransaction::Mode::Cleanup)) { + return nullptr; + } + + // If this is a readwrite transaction to a chrome database make sure the child + // has write access. + // XXX: Maybe add NS_AUUF_OR_WARN_IF also here, see bug 1758875 + if (NS_WARN_IF((aMode == IDBTransaction::Mode::ReadWrite || + aMode == IDBTransaction::Mode::ReadWriteFlush || + aMode == IDBTransaction::Mode::Cleanup) && + mPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo && + !mChromeWriteAccessAllowed)) { + return nullptr; + } + + const ObjectStoreTable& objectStores = mMetadata->mObjectStores; + const uint32_t nameCount = aObjectStoreNames.Length(); + + if (NS_AUUF_OR_WARN_IF(nameCount > objectStores.Count())) { + return nullptr; + } + + QM_TRY_UNWRAP( + auto objectStoreMetadatas, + TransformIntoNewArrayAbortOnErr( + aObjectStoreNames, + [lastName = Maybe<const nsString&>{}, + &objectStores](const nsString& name) mutable + -> mozilla::Result<SafeRefPtr<FullObjectStoreMetadata>, nsresult> { + if (lastName) { + // Make sure that this name is sorted properly and not a + // duplicate. + if (NS_AUUF_OR_WARN_IF(name <= lastName.ref())) { + return Err(NS_ERROR_FAILURE); + } + } + lastName = SomeRef(name); + + const auto foundIt = + std::find_if(objectStores.cbegin(), objectStores.cend(), + [&name](const auto& entry) { + const auto& value = entry.GetData(); + MOZ_ASSERT(entry.GetKey()); + return name == value->mCommonMetadata.name() && + !value->mDeleted; + }); + if (foundIt == objectStores.cend()) { + MOZ_ASSERT_UNLESS_FUZZING(false, "ObjectStore not found."); + return Err(NS_ERROR_FAILURE); + } + + return foundIt->GetData().clonePtr(); + }, + fallible), + nullptr); + + return MakeSafeRefPtr<NormalTransaction>(SafeRefPtrFromThis(), aMode, + std::move(objectStoreMetadatas)) + .forget(); +} + +mozilla::ipc::IPCResult Database::RecvPBackgroundIDBTransactionConstructor( + PBackgroundIDBTransactionParent* aActor, + nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); + MOZ_ASSERT(aMode == IDBTransaction::Mode::ReadOnly || + aMode == IDBTransaction::Mode::ReadWrite || + aMode == IDBTransaction::Mode::ReadWriteFlush || + aMode == IDBTransaction::Mode::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 IPC_OK(); + } + + if (!gConnectionPool) { + gConnectionPool = new ConnectionPool(); + } + + auto* transaction = static_cast<NormalTransaction*>(aActor); + + RefPtr<StartTransactionOp> startOp = new StartTransactionOp( + SafeRefPtr{transaction, AcquireStrongRefFromRawPtr{}}); + + uint64_t transactionId = startOp->StartOnConnectionPool( + GetLoggingInfo()->Id(), mMetadata->mDatabaseId, + transaction->LoggingSerialNumber(), aObjectStoreNames, + aMode != IDBTransaction::Mode::ReadOnly); + + transaction->Init(transactionId); + + if (NS_WARN_IF(!RegisterTransaction(*transaction))) { + IDB_REPORT_INTERNAL_ERR(); + transaction->Abort(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, /* aForce */ false); + return IPC_OK(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Database::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + QM_WARNONLY_TRY(OkIf(PBackgroundIDBDatabaseParent::Send__delete__(this))); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Database::RecvBlocked() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mClosed)) { + return IPC_FAIL(this, "Database already closed!"); + } + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(Id(), &info)); + MOZ_ASSERT(info->mLiveDatabases.Contains(this)); + + if (NS_WARN_IF(!info->mWaitingFactoryOp)) { + return IPC_FAIL(this, "Database info has no mWaitingFactoryOp!"); + } + + info->mWaitingFactoryOp->NoteDatabaseBlocked(this); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Database::RecvClose() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!CloseInternal())) { + return IPC_FAIL(this, "CloseInternal failed!"); + } + + return IPC_OK(); +} + +void Database::StartTransactionOp::RunOnConnectionThread() { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!HasFailed()); + + IDB_LOG_MARK_PARENT_TRANSACTION("Beginning database work", "DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransactionLoggingSerialNumber); + + TransactionDatabaseOperationBase::RunOnConnectionThread(); +} + +nsresult Database::StartTransactionOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + Transaction().SetActiveOnConnectionThread(); + + if (Transaction().GetMode() == IDBTransaction::Mode::Cleanup) { + DebugOnly<nsresult> rv = aConnection->DisableQuotaChecks(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "DisableQuotaChecks failed, trying to continue " + "cleanup transaction with quota checks enabled"); + } + + if (Transaction().GetMode() != IDBTransaction::Mode::ReadOnly) { + QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction())); + } + + 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(SafeRefPtr<Database> aDatabase, Mode aMode) + : mDatabase(std::move(aDatabase)), + mDatabaseId(mDatabase->Id()), + mLoggingSerialNumber( + mDatabase->GetLoggingInfo()->NextTransactionSN(aMode)), + mActiveRequestCount(0), + mInvalidatedOnAnyThread(false), + mMode(aMode), + mResultCode(NS_OK) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabase); + MOZ_ASSERT(mLoggingSerialNumber); +} + +TransactionBase::~TransactionBase() { + MOZ_ASSERT(!mActiveRequestCount); + MOZ_ASSERT(mActorDestroyed); + MOZ_ASSERT_IF(mInitialized, mCommittedOrAborted); +} + +void TransactionBase::Abort(nsresult aResultCode, bool aForce) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aResultCode; + } + + if (aForce) { + mForceAborted.EnsureFlipped(); + } + + MaybeCommitOrAbort(); +} + +mozilla::ipc::IPCResult TransactionBase::RecvCommit( + IProtocol* aActor, const Maybe<int64_t> aLastRequest) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL( + aActor, "Attempt to commit an already comitted/aborted transaction!"); + } + + mCommitOrAbortReceived.Flip(); + mLastRequestBeforeCommit.init(aLastRequest); + MaybeCommitOrAbort(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult TransactionBase::RecvAbort(IProtocol* aActor, + nsresult aResultCode) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(NS_SUCCEEDED(aResultCode))) { + return IPC_FAIL(aActor, "aResultCode must not be a success code!"); + } + + if (NS_WARN_IF(NS_ERROR_GET_MODULE(aResultCode) != + NS_ERROR_MODULE_DOM_INDEXEDDB)) { + return IPC_FAIL(aActor, "aResultCode does not refer to IndexedDB!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL( + aActor, "Attempt to abort an already comitted/aborted transaction!"); + } + + mCommitOrAbortReceived.Flip(); + Abort(aResultCode, /* aForce */ false); + + return IPC_OK(); +} + +void TransactionBase::CommitOrAbort() { + AssertIsOnBackgroundThread(); + + mCommittedOrAborted.Flip(); + + if (!mInitialized) { + return; + } + + // In case of a failed request that was started after committing was + // initiated, abort (cf. + // https://w3c.github.io/IndexedDB/#async-execute-request step 5.3 vs. 5.4). + // Note this can only happen here when we are committing explicitly, otherwise + // the decision is made by the child. + if (NS_SUCCEEDED(mResultCode) && mLastFailedRequest && + *mLastRequestBeforeCommit && + *mLastFailedRequest >= **mLastRequestBeforeCommit) { + mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } + + RefPtr<CommitOp> commitOp = + new CommitOp(SafeRefPtrFromThis(), ClampResultCode(mResultCode)); + + gConnectionPool->Finish(TransactionId(), commitOp); +} + +SafeRefPtr<FullObjectStoreMetadata> +TransactionBase::GetMetadataForObjectStoreId( + IndexOrObjectStoreId aObjectStoreId) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aObjectStoreId); + + if (!aObjectStoreId) { + return nullptr; + } + + auto metadata = mDatabase->Metadata().mObjectStores.Lookup(aObjectStoreId); + if (!metadata || (*metadata)->mDeleted) { + return nullptr; + } + + MOZ_ASSERT((*metadata)->mCommonMetadata.id() == aObjectStoreId); + + return metadata->clonePtr(); +} + +SafeRefPtr<FullIndexMetadata> TransactionBase::GetMetadataForIndexId( + FullObjectStoreMetadata& aObjectStoreMetadata, + IndexOrObjectStoreId aIndexId) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aIndexId); + + if (!aIndexId) { + return nullptr; + } + + auto metadata = aObjectStoreMetadata.mIndexes.Lookup(aIndexId); + if (!metadata || (*metadata)->mDeleted) { + return nullptr; + } + + MOZ_ASSERT((*metadata)->mCommonMetadata.id() == aIndexId); + + return metadata->clonePtr(); +} + +void TransactionBase::NoteModifiedAutoIncrementObjectStore( + const SafeRefPtr<FullObjectStoreMetadata>& aMetadata) { + AssertIsOnConnectionThread(); + + if (!mModifiedAutoIncrementObjectStoreMetadataArray.Contains(aMetadata)) { + mModifiedAutoIncrementObjectStoreMetadataArray.AppendElement( + aMetadata.clonePtr()); + } +} + +void TransactionBase::ForgetModifiedAutoIncrementObjectStore( + FullObjectStoreMetadata& aMetadata) { + AssertIsOnConnectionThread(); + + 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_AUUF_OR_WARN_IF(!VerifyRequestParams(params))) { + return false; + } + break; + } + + case RequestParams::TObjectStorePutParams: { + const ObjectStoreAddPutParams& params = + aParams.get_ObjectStorePutParams().commonParams(); + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params))) { + return false; + } + break; + } + + case RequestParams::TObjectStoreGetParams: { + const ObjectStoreGetParams& params = aParams.get_ObjectStoreGetParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + return false; + } + break; + } + + case RequestParams::TObjectStoreGetKeyParams: { + const ObjectStoreGetKeyParams& params = + aParams.get_ObjectStoreGetKeyParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + return false; + } + break; + } + + case RequestParams::TObjectStoreGetAllParams: { + const ObjectStoreGetAllParams& params = + aParams.get_ObjectStoreGetAllParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + return false; + } + break; + } + + case RequestParams::TObjectStoreGetAllKeysParams: { + const ObjectStoreGetAllKeysParams& params = + aParams.get_ObjectStoreGetAllKeysParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + return false; + } + break; + } + + case RequestParams::TObjectStoreDeleteParams: { + if (NS_AUUF_OR_WARN_IF(mMode != IDBTransaction::Mode::ReadWrite && + mMode != IDBTransaction::Mode::ReadWriteFlush && + mMode != IDBTransaction::Mode::Cleanup && + mMode != IDBTransaction::Mode::VersionChange)) { + return false; + } + + const ObjectStoreDeleteParams& params = + aParams.get_ObjectStoreDeleteParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + return false; + } + break; + } + + case RequestParams::TObjectStoreClearParams: { + if (NS_AUUF_OR_WARN_IF(mMode != IDBTransaction::Mode::ReadWrite && + mMode != IDBTransaction::Mode::ReadWriteFlush && + mMode != IDBTransaction::Mode::Cleanup && + mMode != IDBTransaction::Mode::VersionChange)) { + return false; + } + + const ObjectStoreClearParams& params = + aParams.get_ObjectStoreClearParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + break; + } + + case RequestParams::TObjectStoreCountParams: { + const ObjectStoreCountParams& params = + aParams.get_ObjectStoreCountParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + return false; + } + break; + } + + case RequestParams::TIndexGetParams: { + const IndexGetParams& params = aParams.get_IndexGetParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + const SafeRefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + return false; + } + break; + } + + case RequestParams::TIndexGetKeyParams: { + const IndexGetKeyParams& params = aParams.get_IndexGetKeyParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + const SafeRefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.keyRange()))) { + return false; + } + break; + } + + case RequestParams::TIndexGetAllParams: { + const IndexGetAllParams& params = aParams.get_IndexGetAllParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + const SafeRefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + return false; + } + break; + } + + case RequestParams::TIndexGetAllKeysParams: { + const IndexGetAllKeysParams& params = aParams.get_IndexGetAllKeysParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + const SafeRefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + return false; + } + break; + } + + case RequestParams::TIndexCountParams: { + const IndexCountParams& params = aParams.get_IndexCountParams(); + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + GetMetadataForObjectStoreId(params.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return false; + } + const SafeRefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(*objectStoreMetadata, params.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(params.optionalKeyRange()))) { + 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_AUUF_OR_WARN_IF(aParams.lower().IsUnset())) { + return false; + } + if (NS_AUUF_OR_WARN_IF(!aParams.upper().IsUnset())) { + return false; + } + if (NS_AUUF_OR_WARN_IF(aParams.lowerOpen())) { + return false; + } + if (NS_AUUF_OR_WARN_IF(aParams.upperOpen())) { + return false; + } + } else if (NS_AUUF_OR_WARN_IF(aParams.lower().IsUnset() && + aParams.upper().IsUnset())) { + return false; + } + + return true; +} + +bool TransactionBase::VerifyRequestParams( + const ObjectStoreAddPutParams& aParams) const { + AssertIsOnBackgroundThread(); + + if (NS_AUUF_OR_WARN_IF(mMode != IDBTransaction::Mode::ReadWrite && + mMode != IDBTransaction::Mode::ReadWriteFlush && + mMode != IDBTransaction::Mode::VersionChange)) { + return false; + } + + SafeRefPtr<FullObjectStoreMetadata> objMetadata = + GetMetadataForObjectStoreId(aParams.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objMetadata)) { + return false; + } + + if (NS_AUUF_OR_WARN_IF(!aParams.cloneInfo().data().data.Size())) { + return false; + } + + if (objMetadata->mCommonMetadata.autoIncrement() && + objMetadata->mCommonMetadata.keyPath().IsValid() && + aParams.key().IsUnset()) { + const SerializedStructuredCloneWriteInfo& cloneInfo = aParams.cloneInfo(); + + if (NS_AUUF_OR_WARN_IF(!cloneInfo.offsetToKeyProp())) { + return false; + } + + if (NS_AUUF_OR_WARN_IF(cloneInfo.data().data.Size() < sizeof(uint64_t))) { + return false; + } + + if (NS_AUUF_OR_WARN_IF(cloneInfo.offsetToKeyProp() > + (cloneInfo.data().data.Size() - sizeof(uint64_t)))) { + return false; + } + } else if (NS_AUUF_OR_WARN_IF(aParams.cloneInfo().offsetToKeyProp())) { + return false; + } + + for (const auto& updateInfo : aParams.indexUpdateInfos()) { + SafeRefPtr<FullIndexMetadata> indexMetadata = + GetMetadataForIndexId(*objMetadata, updateInfo.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return false; + } + + if (NS_AUUF_OR_WARN_IF(updateInfo.value().IsUnset())) { + return false; + } + + MOZ_ASSERT(!updateInfo.value().GetBuffer().IsEmpty()); + } + + for (const FileAddInfo& fileAddInfo : aParams.fileAddInfos()) { + const PBackgroundIDBDatabaseFileParent* file = + fileAddInfo.file().AsParent(); + + switch (fileAddInfo.type()) { + case StructuredCloneFileBase::eBlob: + if (NS_AUUF_OR_WARN_IF(!file)) { + return false; + } + break; + + case StructuredCloneFileBase::eMutableFile: { + return false; + } + + case StructuredCloneFileBase::eStructuredClone: + case StructuredCloneFileBase::eWasmBytecode: + case StructuredCloneFileBase::eWasmCompiled: + case StructuredCloneFileBase::eEndGuard: + MOZ_ASSERT_UNLESS_FUZZING(false, "Unsupported."); + return false; + + default: + MOZ_CRASH("Should never get here!"); + } + } + + return true; +} + +bool TransactionBase::VerifyRequestParams( + const Maybe<SerializedKeyRange>& aParams) const { + AssertIsOnBackgroundThread(); + + if (aParams.isSome()) { + if (NS_AUUF_OR_WARN_IF(!VerifyRequestParams(aParams.ref()))) { + return false; + } + } + + return true; +} + +void TransactionBase::NoteActiveRequest() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActiveRequestCount < UINT64_MAX); + + mActiveRequestCount++; +} + +void TransactionBase::NoteFinishedRequest(const int64_t aRequestId, + const nsresult aResultCode) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActiveRequestCount); + + mActiveRequestCount--; + + if (NS_FAILED(aResultCode)) { + mLastFailedRequest = Some(aRequestId); + } + + MaybeCommitOrAbort(); +} + +void TransactionBase::Invalidate() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mInvalidated == mInvalidatedOnAnyThread); + + if (!mInvalidated) { + mInvalidated.Flip(); + mInvalidatedOnAnyThread = true; + + Abort(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, /* aForce */ false); + } +} + +PBackgroundIDBRequestParent* TransactionBase::AllocRequest( + RequestParams&& aParams, bool aTrustParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + aTrustParams = false; +#endif + + if (NS_AUUF_OR_WARN_IF(!aTrustParams && !VerifyRequestParams(aParams))) { + return nullptr; + } + + if (NS_AUUF_OR_WARN_IF(mCommitOrAbortReceived)) { + return nullptr; + } + + RefPtr<NormalTransactionOp> actor; + + switch (aParams.type()) { + case RequestParams::TObjectStoreAddParams: + case RequestParams::TObjectStorePutParams: + actor = new ObjectStoreAddOrPutRequestOp(SafeRefPtrFromThis(), + std::move(aParams)); + break; + + case RequestParams::TObjectStoreGetParams: + actor = new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ false); + break; + + case RequestParams::TObjectStoreGetAllParams: + actor = new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ true); + break; + + case RequestParams::TObjectStoreGetKeyParams: + actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ false); + break; + + case RequestParams::TObjectStoreGetAllKeysParams: + actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ true); + break; + + case RequestParams::TObjectStoreDeleteParams: + actor = new ObjectStoreDeleteRequestOp( + SafeRefPtrFromThis(), aParams.get_ObjectStoreDeleteParams()); + break; + + case RequestParams::TObjectStoreClearParams: + actor = new ObjectStoreClearRequestOp( + SafeRefPtrFromThis(), aParams.get_ObjectStoreClearParams()); + break; + + case RequestParams::TObjectStoreCountParams: + actor = new ObjectStoreCountRequestOp( + SafeRefPtrFromThis(), aParams.get_ObjectStoreCountParams()); + break; + + case RequestParams::TIndexGetParams: + actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ false); + break; + + case RequestParams::TIndexGetKeyParams: + actor = new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ false); + break; + + case RequestParams::TIndexGetAllParams: + actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ true); + break; + + case RequestParams::TIndexGetAllKeysParams: + actor = new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aParams, + /* aGetAll */ true); + break; + + case RequestParams::TIndexCountParams: + actor = new IndexCountRequestOp(SafeRefPtrFromThis(), 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* const aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + const RefPtr<NormalTransactionOp> actor = + dont_AddRef(static_cast<NormalTransactionOp*>(aActor)); + return true; +} + +already_AddRefed<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 + + const OpenCursorParams::Type type = aParams.type(); + SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata; + SafeRefPtr<FullIndexMetadata> indexMetadata; + CursorBase::Direction direction; + + // First extract the parameters common to all open cursor variants. + const auto& commonParams = GetCommonOpenCursorParams(aParams); + objectStoreMetadata = + GetMetadataForObjectStoreId(commonParams.objectStoreId()); + if (NS_AUUF_OR_WARN_IF(!objectStoreMetadata)) { + return nullptr; + } + if (aTrustParams && NS_AUUF_OR_WARN_IF(!VerifyRequestParams( + commonParams.optionalKeyRange()))) { + return nullptr; + } + direction = commonParams.direction(); + + // Now, for the index open cursor variants, extract the additional parameter. + if (type == OpenCursorParams::TIndexOpenCursorParams || + type == OpenCursorParams::TIndexOpenKeyCursorParams) { + const auto& commonIndexParams = GetCommonIndexOpenCursorParams(aParams); + indexMetadata = GetMetadataForIndexId(*objectStoreMetadata, + commonIndexParams.indexId()); + if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { + return nullptr; + } + } + + if (NS_AUUF_OR_WARN_IF(mCommitOrAbortReceived)) { + return nullptr; + } + + // Create Cursor and transfer ownership to IPDL. + switch (type) { + case OpenCursorParams::TObjectStoreOpenCursorParams: + MOZ_ASSERT(!indexMetadata); + return MakeAndAddRef<Cursor<IDBCursorType::ObjectStore>>( + SafeRefPtrFromThis(), std::move(objectStoreMetadata), direction, + CursorBase::ConstructFromTransactionBase{}); + case OpenCursorParams::TObjectStoreOpenKeyCursorParams: + MOZ_ASSERT(!indexMetadata); + return MakeAndAddRef<Cursor<IDBCursorType::ObjectStoreKey>>( + SafeRefPtrFromThis(), std::move(objectStoreMetadata), direction, + CursorBase::ConstructFromTransactionBase{}); + case OpenCursorParams::TIndexOpenCursorParams: + return MakeAndAddRef<Cursor<IDBCursorType::Index>>( + SafeRefPtrFromThis(), std::move(objectStoreMetadata), + std::move(indexMetadata), direction, + CursorBase::ConstructFromTransactionBase{}); + case OpenCursorParams::TIndexOpenKeyCursorParams: + return MakeAndAddRef<Cursor<IDBCursorType::IndexKey>>( + SafeRefPtrFromThis(), std::move(objectStoreMetadata), + std::move(indexMetadata), direction, + CursorBase::ConstructFromTransactionBase{}); + default: + MOZ_CRASH("Cannot get here."); + } +} + +bool TransactionBase::StartCursor(PBackgroundIDBCursorParent* const aActor, + const OpenCursorParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + auto* const op = static_cast<CursorBase*>(aActor); + + if (NS_WARN_IF(!op->Start(aParams))) { + return false; + } + + return true; +} + +/******************************************************************************* + * NormalTransaction + ******************************************************************************/ + +NormalTransaction::NormalTransaction( + SafeRefPtr<Database> aDatabase, TransactionBase::Mode aMode, + nsTArray<SafeRefPtr<FullObjectStoreMetadata>>&& aObjectStores) + : TransactionBase(std::move(aDatabase), aMode), + mObjectStores{std::move(aObjectStores)} { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mObjectStores.IsEmpty()); +} + +bool NormalTransaction::IsSameProcessActor() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* const 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.EnsureFlipped(); + + MaybeCommitOrAbort(); + } +} + +mozilla::ipc::IPCResult NormalTransaction::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + QM_WARNONLY_TRY(OkIf(PBackgroundIDBTransactionParent::Send__delete__(this))); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult NormalTransaction::RecvCommit( + const Maybe<int64_t>& aLastRequest) { + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvCommit(this, aLastRequest); +} + +mozilla::ipc::IPCResult NormalTransaction::RecvAbort( + const nsresult& aResultCode) { + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvAbort(this, aResultCode); +} + +PBackgroundIDBRequestParent* +NormalTransaction::AllocPBackgroundIDBRequestParent( + const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + return AllocRequest(std::move(const_cast<RequestParams&>(aParams)), + IsSameProcessActor()); +} + +mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBRequestConstructor( + PBackgroundIDBRequestParent* const aActor, const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + if (!StartRequest(aActor)) { + return IPC_FAIL(this, "StartRequest failed!"); + } + return IPC_OK(); +} + +bool NormalTransaction::DeallocPBackgroundIDBRequestParent( + PBackgroundIDBRequestParent* const aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return DeallocRequest(aActor); +} + +already_AddRefed<PBackgroundIDBCursorParent> +NormalTransaction::AllocPBackgroundIDBCursorParent( + const OpenCursorParams& aParams) { + AssertIsOnBackgroundThread(); + + return AllocCursor(aParams, IsSameProcessActor()); +} + +mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBCursorConstructor( + PBackgroundIDBCursorParent* const aActor, const OpenCursorParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + if (!StartCursor(aActor, aParams)) { + return IPC_FAIL(this, "StartCursor failed!"); + } + return IPC_OK(); +} + +/******************************************************************************* + * VersionChangeTransaction + ******************************************************************************/ + +VersionChangeTransaction::VersionChangeTransaction( + OpenDatabaseOp* aOpenDatabaseOp) + : TransactionBase(aOpenDatabaseOp->mDatabase.clonePtr(), + IDBTransaction::Mode::VersionChange), + mOpenDatabaseOp(aOpenDatabaseOp) { + 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(!IsActorDestroyed()); + + mActorWasAlive.Flip(); +} + +bool VersionChangeTransaction::CopyDatabaseMetadata() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mOldMetadata); + + const auto& origMetadata = GetDatabase().Metadata(); + + SafeRefPtr<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 = std::move(info->mMetadata); + info->mMetadata = std::move(newMetadata); + + // Replace metadata pointers for all live databases. + for (const auto& liveDatabase : info->mLiveDatabases) { + liveDatabase->mMetadata = info->mMetadata.clonePtr(); + } + + return true; +} + +void VersionChangeTransaction::UpdateMetadata(nsresult aResult) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT(!!mActorWasAlive == !!mOpenDatabaseOp->mDatabase); + MOZ_ASSERT_IF(mActorWasAlive, !mOpenDatabaseOp->mDatabaseId.IsEmpty()); + + if (IsActorDestroyed() || !mActorWasAlive) { + return; + } + + SafeRefPtr<FullDatabaseMetadata> oldMetadata = std::move(mOldMetadata); + + 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. + info->mMetadata->mObjectStores.RemoveIf([](const auto& objectStoreIter) { + MOZ_ASSERT(objectStoreIter.Key()); + const SafeRefPtr<FullObjectStoreMetadata>& metadata = + objectStoreIter.Data(); + MOZ_ASSERT(metadata); + + if (metadata->mDeleted) { + return true; + } + + metadata->mIndexes.RemoveIf([](const auto& indexIter) -> bool { + MOZ_ASSERT(indexIter.Key()); + const SafeRefPtr<FullIndexMetadata>& index = indexIter.Data(); + MOZ_ASSERT(index); + + return index->mDeleted; + }); + metadata->mIndexes.MarkImmutable(); + + return false; + }); + + info->mMetadata->mObjectStores.MarkImmutable(); + } else { + // Replace metadata pointers for all live databases. + info->mMetadata = std::move(oldMetadata); + + for (auto& liveDatabase : info->mLiveDatabases) { + liveDatabase->mMetadata = info->mMetadata.clonePtr(); + } + } +} + +void VersionChangeTransaction::SendCompleteNotification(nsresult aResult) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mOpenDatabaseOp); + MOZ_ASSERT_IF(!mActorWasAlive, mOpenDatabaseOp->HasFailed()); + MOZ_ASSERT_IF(!mActorWasAlive, mOpenDatabaseOp->mState > + OpenDatabaseOp::State::SendingResults); + + const RefPtr<OpenDatabaseOp> openDatabaseOp = std::move(mOpenDatabaseOp); + + if (!mActorWasAlive) { + return; + } + + if (NS_FAILED(aResult)) { + // 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->SetFailureCodeIfUnset(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.EnsureFlipped(); + + MaybeCommitOrAbort(); + } +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + QM_WARNONLY_TRY( + OkIf(PBackgroundIDBVersionChangeTransactionParent::Send__delete__(this))); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvCommit( + const Maybe<int64_t>& aLastRequest) { + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvCommit(this, aLastRequest); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvAbort( + const nsresult& aResultCode) { + AssertIsOnBackgroundThread(); + + return TransactionBase::RecvAbort(this, aResultCode); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvCreateObjectStore( + const ObjectStoreMetadata& aMetadata) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aMetadata.id())) { + return IPC_FAIL(this, "No metadata ID!"); + } + + const SafeRefPtr<FullDatabaseMetadata> dbMetadata = + GetDatabase().MetadataPtr(); + + if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextObjectStoreId)) { + return IPC_FAIL(this, "Requested metadata ID does not match next ID!"); + } + + if (NS_WARN_IF( + MatchMetadataNameOrId(dbMetadata->mObjectStores, aMetadata.id(), + SomeRef<const nsAString&>(aMetadata.name())) + .isSome())) { + return IPC_FAIL(this, "MatchMetadataNameOrId failed!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + const int64_t initialAutoIncrementId = aMetadata.autoIncrement() ? 1 : 0; + auto newMetadata = MakeSafeRefPtr<FullObjectStoreMetadata>( + aMetadata, FullObjectStoreMetadata::AutoIncrementIds{ + initialAutoIncrementId, initialAutoIncrementId}); + + if (NS_WARN_IF(!dbMetadata->mObjectStores.InsertOrUpdate( + aMetadata.id(), std::move(newMetadata), fallible))) { + return IPC_FAIL(this, "mObjectStores.InsertOrUpdate failed!"); + } + + dbMetadata->mNextObjectStoreId++; + + RefPtr<CreateObjectStoreOp> op = new CreateObjectStoreOp( + SafeRefPtrFromThis().downcast<VersionChangeTransaction>(), aMetadata); + + if (NS_WARN_IF(!op->Init(*this))) { + op->Cleanup(); + return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); + } + + op->DispatchToConnectionPool(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvDeleteObjectStore( + const IndexOrObjectStoreId& aObjectStoreId) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + return IPC_FAIL(this, "No ObjectStoreId!"); + } + + const auto& dbMetadata = GetDatabase().Metadata(); + MOZ_ASSERT(dbMetadata.mNextObjectStoreId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata.mNextObjectStoreId)) { + return IPC_FAIL(this, "Invalid ObjectStoreId!"); + } + + SafeRefPtr<FullObjectStoreMetadata> foundMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundMetadata)) { + return IPC_FAIL(this, "No metadata found for ObjectStoreId!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + foundMetadata->mDeleted.Flip(); + + DebugOnly<bool> foundTargetId = false; + const bool isLastObjectStore = std::all_of( + dbMetadata.mObjectStores.begin(), dbMetadata.mObjectStores.end(), + [&foundTargetId, aObjectStoreId](const auto& objectStoreEntry) -> bool { + if (uint64_t(aObjectStoreId) == objectStoreEntry.GetKey()) { + foundTargetId = true; + return true; + } + + return objectStoreEntry.GetData()->mDeleted; + }); + MOZ_ASSERT_IF(isLastObjectStore, foundTargetId); + + RefPtr<DeleteObjectStoreOp> op = new DeleteObjectStoreOp( + SafeRefPtrFromThis().downcast<VersionChangeTransaction>(), + std::move(foundMetadata), isLastObjectStore); + + if (NS_WARN_IF(!op->Init(*this))) { + op->Cleanup(); + return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); + } + + op->DispatchToConnectionPool(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvRenameObjectStore( + const IndexOrObjectStoreId& aObjectStoreId, const nsAString& aName) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + return IPC_FAIL(this, "No ObjectStoreId!"); + } + + { + const auto& dbMetadata = GetDatabase().Metadata(); + MOZ_ASSERT(dbMetadata.mNextObjectStoreId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata.mNextObjectStoreId)) { + return IPC_FAIL(this, "Invalid ObjectStoreId!"); + } + } + + SafeRefPtr<FullObjectStoreMetadata> foundMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundMetadata)) { + return IPC_FAIL(this, "No metadata found for ObjectStoreId!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + foundMetadata->mCommonMetadata.name() = aName; + + RefPtr<RenameObjectStoreOp> renameOp = new RenameObjectStoreOp( + SafeRefPtrFromThis().downcast<VersionChangeTransaction>(), + *foundMetadata); + + if (NS_WARN_IF(!renameOp->Init(*this))) { + renameOp->Cleanup(); + return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); + } + + renameOp->DispatchToConnectionPool(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvCreateIndex( + const IndexOrObjectStoreId& aObjectStoreId, + const IndexMetadata& aMetadata) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + return IPC_FAIL(this, "No ObjectStoreId!"); + } + + if (NS_WARN_IF(!aMetadata.id())) { + return IPC_FAIL(this, "No Metadata id!"); + } + + const auto dbMetadata = GetDatabase().MetadataPtr(); + + if (NS_WARN_IF(aMetadata.id() != dbMetadata->mNextIndexId)) { + return IPC_FAIL(this, "Requested metadata ID does not match next ID!"); + } + + SafeRefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundObjectStoreMetadata)) { + return IPC_FAIL(this, "GetMetadataForObjectStoreId failed!"); + } + + if (NS_WARN_IF(MatchMetadataNameOrId( + foundObjectStoreMetadata->mIndexes, aMetadata.id(), + SomeRef<const nsAString&>(aMetadata.name())) + .isSome())) { + return IPC_FAIL(this, "MatchMetadataNameOrId failed!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + auto newMetadata = MakeSafeRefPtr<FullIndexMetadata>(); + newMetadata->mCommonMetadata = aMetadata; + + if (NS_WARN_IF(!foundObjectStoreMetadata->mIndexes.InsertOrUpdate( + aMetadata.id(), std::move(newMetadata), fallible))) { + return IPC_FAIL(this, "mIndexes.InsertOrUpdate failed!"); + } + + dbMetadata->mNextIndexId++; + + RefPtr<CreateIndexOp> op = new CreateIndexOp( + SafeRefPtrFromThis().downcast<VersionChangeTransaction>(), aObjectStoreId, + aMetadata); + + if (NS_WARN_IF(!op->Init(*this))) { + op->Cleanup(); + return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); + } + + op->DispatchToConnectionPool(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvDeleteIndex( + const IndexOrObjectStoreId& aObjectStoreId, + const IndexOrObjectStoreId& aIndexId) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + return IPC_FAIL(this, "No ObjectStoreId!"); + } + + if (NS_WARN_IF(!aIndexId)) { + return IPC_FAIL(this, "No Index id!"); + } + { + const auto& dbMetadata = GetDatabase().Metadata(); + MOZ_ASSERT(dbMetadata.mNextObjectStoreId > 0); + MOZ_ASSERT(dbMetadata.mNextIndexId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata.mNextObjectStoreId)) { + return IPC_FAIL(this, "Requested ObjectStoreId does not match next ID!"); + } + + if (NS_WARN_IF(aIndexId >= dbMetadata.mNextIndexId)) { + return IPC_FAIL(this, "Requested IndexId does not match next ID!"); + } + } + + SafeRefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundObjectStoreMetadata)) { + return IPC_FAIL(this, "GetMetadataForObjectStoreId failed!"); + } + + SafeRefPtr<FullIndexMetadata> foundIndexMetadata = + GetMetadataForIndexId(*foundObjectStoreMetadata, aIndexId); + + if (NS_WARN_IF(!foundIndexMetadata)) { + return IPC_FAIL(this, "GetMetadataForIndexId failed!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + foundIndexMetadata->mDeleted.Flip(); + + DebugOnly<bool> foundTargetId = false; + const bool isLastIndex = + std::all_of(foundObjectStoreMetadata->mIndexes.cbegin(), + foundObjectStoreMetadata->mIndexes.cend(), + [&foundTargetId, aIndexId](const auto& indexEntry) -> bool { + if (uint64_t(aIndexId) == indexEntry.GetKey()) { + foundTargetId = true; + return true; + } + + return indexEntry.GetData()->mDeleted; + }); + MOZ_ASSERT_IF(isLastIndex, foundTargetId); + + RefPtr<DeleteIndexOp> op = new DeleteIndexOp( + SafeRefPtrFromThis().downcast<VersionChangeTransaction>(), aObjectStoreId, + aIndexId, foundIndexMetadata->mCommonMetadata.unique(), isLastIndex); + + if (NS_WARN_IF(!op->Init(*this))) { + op->Cleanup(); + return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); + } + + op->DispatchToConnectionPool(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult VersionChangeTransaction::RecvRenameIndex( + const IndexOrObjectStoreId& aObjectStoreId, + const IndexOrObjectStoreId& aIndexId, const nsAString& aName) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!aObjectStoreId)) { + return IPC_FAIL(this, "No ObjectStoreId!"); + } + + if (NS_WARN_IF(!aIndexId)) { + return IPC_FAIL(this, "No Index id!"); + } + + const SafeRefPtr<FullDatabaseMetadata> dbMetadata = + GetDatabase().MetadataPtr(); + MOZ_ASSERT(dbMetadata); + MOZ_ASSERT(dbMetadata->mNextObjectStoreId > 0); + MOZ_ASSERT(dbMetadata->mNextIndexId > 0); + + if (NS_WARN_IF(aObjectStoreId >= dbMetadata->mNextObjectStoreId)) { + return IPC_FAIL(this, "Requested ObjectStoreId does not match next ID!"); + } + + if (NS_WARN_IF(aIndexId >= dbMetadata->mNextIndexId)) { + return IPC_FAIL(this, "Requested IndexId does not match next ID!"); + } + + SafeRefPtr<FullObjectStoreMetadata> foundObjectStoreMetadata = + GetMetadataForObjectStoreId(aObjectStoreId); + + if (NS_WARN_IF(!foundObjectStoreMetadata)) { + return IPC_FAIL(this, "GetMetadataForObjectStoreId failed!"); + } + + SafeRefPtr<FullIndexMetadata> foundIndexMetadata = + GetMetadataForIndexId(*foundObjectStoreMetadata, aIndexId); + + if (NS_WARN_IF(!foundIndexMetadata)) { + return IPC_FAIL(this, "GetMetadataForIndexId failed!"); + } + + if (NS_WARN_IF(mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + foundIndexMetadata->mCommonMetadata.name() = aName; + + RefPtr<RenameIndexOp> renameOp = new RenameIndexOp( + SafeRefPtrFromThis().downcast<VersionChangeTransaction>(), + *foundIndexMetadata, aObjectStoreId); + + if (NS_WARN_IF(!renameOp->Init(*this))) { + renameOp->Cleanup(); + return IPC_FAIL(this, "ObjectStoreOp initialization failed!"); + } + + renameOp->DispatchToConnectionPool(); + + return IPC_OK(); +} + +PBackgroundIDBRequestParent* +VersionChangeTransaction::AllocPBackgroundIDBRequestParent( + const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + return AllocRequest(std::move(const_cast<RequestParams&>(aParams)), + IsSameProcessActor()); +} + +mozilla::ipc::IPCResult +VersionChangeTransaction::RecvPBackgroundIDBRequestConstructor( + PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + if (!StartRequest(aActor)) { + return IPC_FAIL(this, "StartRequest failed!"); + } + return IPC_OK(); +} + +bool VersionChangeTransaction::DeallocPBackgroundIDBRequestParent( + PBackgroundIDBRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + return DeallocRequest(aActor); +} + +already_AddRefed<PBackgroundIDBCursorParent> +VersionChangeTransaction::AllocPBackgroundIDBCursorParent( + const OpenCursorParams& aParams) { + AssertIsOnBackgroundThread(); + + return AllocCursor(aParams, IsSameProcessActor()); +} + +mozilla::ipc::IPCResult +VersionChangeTransaction::RecvPBackgroundIDBCursorConstructor( + PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + if (!StartCursor(aActor, aParams)) { + return IPC_FAIL(this, "StartCursor failed!"); + } + return IPC_OK(); +} + +/******************************************************************************* + * CursorBase + ******************************************************************************/ + +CursorBase::CursorBase(SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<FullObjectStoreMetadata> aObjectStoreMetadata, + const Direction aDirection, + const ConstructFromTransactionBase /*aConstructionTag*/) + : mTransaction(std::move(aTransaction)), + mObjectStoreMetadata(WrapNotNull(std::move(aObjectStoreMetadata))), + mObjectStoreId((*mObjectStoreMetadata)->mCommonMetadata.id()), + mDirection(aDirection), + mMaxExtraCount(IndexedDatabaseManager::MaxPreloadExtraRecords()), + mIsSameProcessActor(!BackgroundParent::IsOtherProcessActor( + mTransaction->GetBackgroundParent())) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction); + + static_assert( + OpenCursorParams::T__None == 0 && OpenCursorParams::T__Last == 4, + "Lots of code here assumes only four types of cursors!"); +} + +template <IDBCursorType CursorType> +bool Cursor<CursorType>::VerifyRequestParams( + const CursorRequestParams& aParams, + const CursorPosition<CursorType>& aPosition) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); + MOZ_ASSERT(this->mObjectStoreMetadata); + if constexpr (IsIndexCursor) { + MOZ_ASSERT(this->mIndexMetadata); + } + +#ifdef DEBUG + { + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + mTransaction->GetMetadataForObjectStoreId(mObjectStoreId); + if (objectStoreMetadata) { + MOZ_ASSERT(objectStoreMetadata == (*this->mObjectStoreMetadata)); + } else { + MOZ_ASSERT((*this->mObjectStoreMetadata)->mDeleted); + } + + if constexpr (IsIndexCursor) { + if (objectStoreMetadata) { + const SafeRefPtr<FullIndexMetadata> indexMetadata = + mTransaction->GetMetadataForIndexId(*objectStoreMetadata, + this->mIndexId); + if (indexMetadata) { + MOZ_ASSERT(indexMetadata == *this->mIndexMetadata); + } else { + MOZ_ASSERT((*this->mIndexMetadata)->mDeleted); + } + } + } + } +#endif + + if (NS_AUUF_OR_WARN_IF((*this->mObjectStoreMetadata)->mDeleted)) { + return false; + } + + if constexpr (IsIndexCursor) { + if (NS_AUUF_OR_WARN_IF(this->mIndexMetadata && + (*this->mIndexMetadata)->mDeleted)) { + return false; + } + } + + const Key& sortKey = aPosition.GetSortKey(this->IsLocaleAware()); + + switch (aParams.type()) { + case CursorRequestParams::TContinueParams: { + const Key& key = aParams.get_ContinueParams().key(); + if (!key.IsUnset()) { + switch (mDirection) { + case IDBCursorDirection::Next: + case IDBCursorDirection::Nextunique: + if (NS_AUUF_OR_WARN_IF(key <= sortKey)) { + return false; + } + break; + + case IDBCursorDirection::Prev: + case IDBCursorDirection::Prevunique: + if (NS_AUUF_OR_WARN_IF(key >= sortKey)) { + return false; + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + } + break; + } + + case CursorRequestParams::TContinuePrimaryKeyParams: { + if constexpr (IsIndexCursor) { + 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 IDBCursorDirection::Next: + if (NS_AUUF_OR_WARN_IF(key < sortKey || + (key == sortKey && + primaryKey <= aPosition.mObjectStoreKey))) { + return false; + } + break; + + case IDBCursorDirection::Prev: + if (NS_AUUF_OR_WARN_IF(key > sortKey || + (key == sortKey && + primaryKey >= aPosition.mObjectStoreKey))) { + return false; + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + } + break; + } + + case CursorRequestParams::TAdvanceParams: + if (NS_AUUF_OR_WARN_IF(!aParams.get_AdvanceParams().count())) { + return false; + } + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +template <IDBCursorType CursorType> +bool Cursor<CursorType>::Start(const OpenCursorParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() == ToOpenCursorParamsType(CursorType)); + MOZ_ASSERT(this->mObjectStoreMetadata); + + if (NS_AUUF_OR_WARN_IF(mCurrentlyRunningOp)) { + return false; + } + + const Maybe<SerializedKeyRange>& optionalKeyRange = + GetCommonOpenCursorParams(aParams).optionalKeyRange(); + + const 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 ValueCursorBase::ProcessFiles(CursorResponse& aResponse, + const FilesArray& aFiles) { + MOZ_ASSERT_IF( + aResponse.type() == CursorResponse::Tnsresult || + aResponse.type() == CursorResponse::Tvoid_t || + aResponse.type() == + CursorResponse::TArrayOfObjectStoreKeyCursorResponse || + aResponse.type() == CursorResponse::TArrayOfIndexKeyCursorResponse, + aFiles.IsEmpty()); + + for (size_t i = 0; i < aFiles.Length(); ++i) { + const auto& files = aFiles[i]; + if (!files.IsEmpty()) { + // TODO: Replace this assertion by one that checks if the response type + // matches the cursor type, at a more generic location. + MOZ_ASSERT(aResponse.type() == + CursorResponse::TArrayOfObjectStoreCursorResponse || + aResponse.type() == + CursorResponse::TArrayOfIndexCursorResponse); + + 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::TArrayOfIndexCursorResponse: { + auto& responses = aResponse.get_ArrayOfIndexCursorResponse(); + MOZ_ASSERT(i < responses.Length()); + serializedInfo = &responses[i].cloneInfo(); + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + MOZ_ASSERT(serializedInfo); + MOZ_ASSERT(serializedInfo->files().IsEmpty()); + MOZ_ASSERT(this->mDatabase); + + QM_TRY_UNWRAP(serializedInfo->files(), + SerializeStructuredCloneFiles(this->mDatabase, files, + /* aForPreprocess */ false), + QM_VOID, [&aResponse](const nsresult result) { + aResponse = ClampResultCode(result); + }); + } + } +} + +template <IDBCursorType CursorType> +void Cursor<CursorType>::SendResponseInternal( + CursorResponse& aResponse, const FilesArrayT<CursorType>& 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(this->mObjectStoreMetadata); + MOZ_ASSERT(mCurrentlyRunningOp); + + KeyValueBase::ProcessFiles(aResponse, aFiles); + + // Work around the deleted function by casting to the base class. + QM_WARNONLY_TRY(OkIf( + static_cast<PBackgroundIDBCursorParent*>(this)->SendResponse(aResponse))); + + mCurrentlyRunningOp = nullptr; +} + +template <IDBCursorType CursorType> +void Cursor<CursorType>::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + + if (mCurrentlyRunningOp) { + mCurrentlyRunningOp->NoteActorDestroyed(); + } + + if constexpr (IsValueCursor) { + this->mBackgroundParent.destroy(); + } + this->mObjectStoreMetadata.destroy(); + if constexpr (IsIndexCursor) { + this->mIndexMetadata.destroy(); + } +} + +template <IDBCursorType CursorType> +mozilla::ipc::IPCResult Cursor<CursorType>::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(this->mObjectStoreMetadata); + + if (NS_WARN_IF(mCurrentlyRunningOp)) { + return IPC_FAIL( + this, + "Attempt to delete a cursor with a non-null mCurrentlyRunningOp!"); + } + + QM_WARNONLY_TRY(OkIf(PBackgroundIDBCursorParent::Send__delete__(this))); + + return IPC_OK(); +} + +template <IDBCursorType CursorType> +mozilla::ipc::IPCResult Cursor<CursorType>::RecvContinue( + const CursorRequestParams& aParams, const Key& aCurrentKey, + const Key& aCurrentObjectStoreKey) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); + MOZ_ASSERT(this->mObjectStoreMetadata); + if constexpr (IsIndexCursor) { + MOZ_ASSERT(this->mIndexMetadata); + } + + const bool trustParams = +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + false +#else + this->mIsSameProcessActor +#endif + ; + + MOZ_ASSERT(!aCurrentKey.IsUnset()); + + QM_TRY_UNWRAP( + auto position, + ([&]() -> Result<CursorPosition<CursorType>, mozilla::ipc::IPCResult> { + if constexpr (IsIndexCursor) { + auto localeAwarePosition = Key{}; + if (this->IsLocaleAware()) { + QM_TRY_UNWRAP( + localeAwarePosition, + aCurrentKey.ToLocaleAwareKey(this->mLocale), + Err(IPC_FAIL(this, "aCurrentKey.ToLocaleAwareKey failed!"))); + } + return CursorPosition<CursorType>{aCurrentKey, localeAwarePosition, + aCurrentObjectStoreKey}; + } else { + return CursorPosition<CursorType>{aCurrentKey}; + } + }())); + + if (!trustParams && !VerifyRequestParams(aParams, position)) { + return IPC_FAIL(this, "VerifyRequestParams failed!"); + } + + if (NS_WARN_IF(mCurrentlyRunningOp)) { + return IPC_FAIL(this, "Cursor is CurrentlyRunningOp!"); + } + + if (NS_WARN_IF(mTransaction->mCommitOrAbortReceived)) { + return IPC_FAIL(this, "Transaction is already committed/aborted!"); + } + + const RefPtr<ContinueOp> continueOp = + new ContinueOp(this, aParams, std::move(position)); + if (NS_WARN_IF(!continueOp->Init(*mTransaction))) { + continueOp->Cleanup(); + return IPC_FAIL(this, "ContinueOp initialization failed!"); + } + + continueOp->DispatchToConnectionPool(); + mCurrentlyRunningOp = continueOp; + + return IPC_OK(); +} + +/******************************************************************************* + * DatabaseFileManager + ******************************************************************************/ + +DatabaseFileManager::MutexType DatabaseFileManager::sMutex; + +DatabaseFileManager::DatabaseFileManager( + PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + const nsAString& aDatabaseName, const nsCString& aDatabaseID, + bool aEnforcingQuota, bool aIsInPrivateBrowsingMode) + : mPersistenceType(aPersistenceType), + mOriginMetadata(aOriginMetadata), + mDatabaseName(aDatabaseName), + mDatabaseID(aDatabaseID), + mEnforcingQuota(aEnforcingQuota), + mIsInPrivateBrowsingMode(aIsInPrivateBrowsingMode) {} + +nsresult DatabaseFileManager::Init(nsIFile* aDirectory, + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + { + QM_TRY_INSPECT(const bool& existsAsDirectory, + ExistsAsDirectory(*aDirectory)); + + if (!existsAsDirectory) { + QM_TRY(MOZ_TO_RESULT(aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755))); + } + + QM_TRY_UNWRAP(auto path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, aDirectory, GetPath)); + + mDirectoryPath.init(std::move(path)); + } + + QM_TRY_INSPECT(const auto& journalDirectory, + CloneFileAndAppend(*aDirectory, kJournalDirectoryName)); + + // We don't care if it doesn't exist at all, but if it does exist, make sure + // it's a directory. + QM_TRY_INSPECT(const bool& existsAsDirectory, + ExistsAsDirectory(*journalDirectory)); + Unused << existsAsDirectory; + + { + QM_TRY_UNWRAP(auto path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, journalDirectory, GetPath)); + + mJournalDirectoryPath.init(std::move(path)); + } + + QM_TRY_INSPECT(const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, "SELECT id, refcount FROM file"_ns)); + + QM_TRY( + CollectWhileHasResult(*stmt, [this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const int64_t& id, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0)); + QM_TRY_INSPECT(const int32_t& dbRefCnt, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 1)); + + // We put a raw pointer into the hash table, so the memory refcount will + // be 0, but the dbRefCnt is non-zero, which will keep the + // DatabaseFileInfo object alive. + MOZ_ASSERT(dbRefCnt > 0); + mFileInfos.InsertOrUpdate( + id, MakeNotNull<DatabaseFileInfo*>( + FileInfoManagerGuard{}, SafeRefPtrFromThis(), id, + static_cast<nsrefcnt>(dbRefCnt))); + + mLastFileId = std::max(id, mLastFileId); + + return Ok{}; + })); + + mInitialized.Flip(); + + return NS_OK; +} + +nsCOMPtr<nsIFile> DatabaseFileManager::GetDirectory() { + if (!this->AssertValid()) { + return nullptr; + } + + return GetFileForPath(*mDirectoryPath); +} + +nsCOMPtr<nsIFile> DatabaseFileManager::GetCheckedDirectory() { + auto 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; +} + +nsCOMPtr<nsIFile> DatabaseFileManager::GetJournalDirectory() { + if (!this->AssertValid()) { + return nullptr; + } + + return GetFileForPath(*mJournalDirectoryPath); +} + +nsCOMPtr<nsIFile> DatabaseFileManager::EnsureJournalDirectory() { + // This can happen on the IO or on a transaction thread. + MOZ_ASSERT(!NS_IsMainThread()); + + auto journalDirectory = GetFileForPath(*mJournalDirectoryPath); + QM_TRY(OkIf(journalDirectory), nullptr); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, Exists), + nullptr); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, IsDirectory), + nullptr); + + QM_TRY(OkIf(isDirectory), nullptr); + } else { + QM_TRY( + MOZ_TO_RESULT(journalDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)), + nullptr); + } + + return journalDirectory; +} + +// static +nsCOMPtr<nsIFile> DatabaseFileManager::GetFileForId(nsIFile* aDirectory, + int64_t aId) { + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(aId > 0); + + QM_TRY_RETURN(CloneFileAndAppend(*aDirectory, IntToString(aId)), nullptr); +} + +// static +nsCOMPtr<nsIFile> DatabaseFileManager::GetCheckedFileForId(nsIFile* aDirectory, + int64_t aId) { + auto 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; +} + +// static +nsresult DatabaseFileManager::InitDirectory(nsIFile& aDirectory, + nsIFile& aDatabaseFile, + const nsACString& aOrigin, + uint32_t aTelemetryId) { + AssertIsOnIOThread(); + + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); + + if (!exists) { + return NS_OK; + } + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory)); + QM_TRY(OkIf(isDirectory), NS_ERROR_FAILURE); + } + + QM_TRY_INSPECT(const auto& journalDirectory, + CloneFileAndAppend(aDirectory, kJournalDirectoryName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, Exists)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(journalDirectory, IsDirectory)); + QM_TRY(OkIf(isDirectory), NS_ERROR_FAILURE); + + bool hasJournals = false; + + QM_TRY(CollectEachFile( + *journalDirectory, + [&hasJournals](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); + + nsresult rv; + leafName.ToInteger64(&rv); + if (NS_SUCCEEDED(rv)) { + hasJournals = true; + } else { + UNKNOWN_FILE_WARNING(leafName); + } + + return Ok{}; + })); + + if (hasJournals) { + QM_TRY_UNWRAP(const NotNull<nsCOMPtr<mozIStorageConnection>> connection, + CreateStorageConnection( + aDatabaseFile, aDirectory, VoidString(), aOrigin, + /* aDirectoryLockId */ -1, aTelemetryId, Nothing{})); + + mozStorageTransaction transaction(connection.get(), false); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())) + + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL( + "CREATE VIRTUAL TABLE fs USING filesystem;"_ns))); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, *connection, CreateStatement, + "SELECT name, (name IN (SELECT id FROM file)) FROM fs WHERE path = :path"_ns)); + + QM_TRY_INSPECT(const auto& path, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, journalDirectory, GetPath)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindStringByIndex(0, path))); + + QM_TRY(CollectWhileHasResult( + *stmt, + [&aDirectory, &journalDirectory](auto& stmt) -> Result<Ok, nsresult> { + nsString name; + QM_TRY(MOZ_TO_RESULT(stmt.GetString(0, name))); + + nsresult rv; + name.ToInteger64(&rv); + if (NS_FAILED(rv)) { + return Ok{}; + } + + int32_t flag = stmt.AsInt32(1); + + if (!flag) { + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend(aDirectory, name)); + + if (NS_FAILED(file->Remove(false))) { + NS_WARNING("Failed to remove orphaned file!"); + } + } + + QM_TRY_INSPECT(const auto& journalFile, + CloneFileAndAppend(*journalDirectory, name)); + + if (NS_FAILED(journalFile->Remove(false))) { + NS_WARNING("Failed to remove journal file!"); + } + + return Ok{}; + })); + + QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("DROP TABLE fs;"_ns))); + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + } + } + + return NS_OK; +} + +// static +Result<FileUsageType, nsresult> DatabaseFileManager::GetUsage( + nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + FileUsageType usage; + + QM_TRY(TraverseFiles( + *aDirectory, + // KnownDirEntryOp + [&usage](nsIFile& file, const bool isDirectory) -> Result<Ok, nsresult> { + if (isDirectory) { + return Ok{}; + } + + // Usually we only use QM_OR_ELSE_LOG_VERBOSE(_IF) with Remove and + // NS_ERROR_FILE_NOT_FOUND check, but the file was found by a directory + // traversal and ToInteger on the name succeeded, so it should be our + // file and if the file disappears, the use of QM_OR_ELSE_WARN_IF is ok + // here. + QM_TRY_INSPECT(const auto& thisUsage, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(file, GetFileSize) + .map([](const int64_t fileSize) { + return FileUsageType(Some(uint64_t(fileSize))); + }), + // Predicate. + ([](const nsresult rv) { + return rv == NS_ERROR_FILE_NOT_FOUND; + }), + // Fallback. If the file does no longer exist, treat + // it as 0-sized. + ErrToDefaultOk<FileUsageType>)); + + usage += thisUsage; + + return Ok{}; + }, + // UnknownDirEntryOp + [](nsIFile&, const bool) -> Result<Ok, nsresult> { return Ok{}; })); + + return usage; +} + +nsresult DatabaseFileManager::SyncDeleteFile(const int64_t aId) { + MOZ_ASSERT(!mFileInfos.Contains(aId)); + + if (!this->AssertValid()) { + return NS_ERROR_UNEXPECTED; + } + + const auto directory = GetDirectory(); + QM_TRY(OkIf(directory), NS_ERROR_FAILURE); + + const auto journalDirectory = GetJournalDirectory(); + QM_TRY(OkIf(journalDirectory), NS_ERROR_FAILURE); + + const nsCOMPtr<nsIFile> file = GetFileForId(directory, aId); + QM_TRY(OkIf(file), NS_ERROR_FAILURE); + + const nsCOMPtr<nsIFile> journalFile = GetFileForId(journalDirectory, aId); + QM_TRY(OkIf(journalFile), NS_ERROR_FAILURE); + + return SyncDeleteFile(*file, *journalFile); +} + +nsresult DatabaseFileManager::SyncDeleteFile(nsIFile& aFile, + nsIFile& aJournalFile) const { + QuotaManager* const quotaManager = + EnforcingQuota() ? QuotaManager::Get() : nullptr; + MOZ_ASSERT_IF(EnforcingQuota(), quotaManager); + + QM_TRY(MOZ_TO_RESULT(DeleteFile(aFile, quotaManager, Type(), OriginMetadata(), + Idempotency::No))); + + QM_TRY(MOZ_TO_RESULT(aJournalFile.Remove(false))); + + return NS_OK; +} + +/******************************************************************************* + * QuotaClient + ******************************************************************************/ + +QuotaClient* QuotaClient::sInstance = nullptr; + +QuotaClient::QuotaClient() : mDeleteTimer(NS_NewTimer()) { + 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"); + + gStorageDatabaseNameMutex = new Mutex("IndexedDB gStorageDatabaseNameMutex"); + + 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; + + gStorageDatabaseNameHashtable = nullptr; + gStorageDatabaseNameMutex = nullptr; + + sInstance = nullptr; +} + +nsresult QuotaClient::AsyncDeleteFile(DatabaseFileManager* aFileManager, + int64_t aFileId) { + AssertIsOnBackgroundThread(); + + if (IsShuttingDownOnBackgroundThread()) { + // Whoops! We want to delete an IndexedDB disk-backed File but it's too late + // to actually delete the file! This means we're going to "leak" the file + // and leave it around when we shouldn't! (The file will stay around until + // next storage initialization is triggered when the app is started again). + // Fixing this is tracked by bug 1539377. + + return NS_OK; + } + + MOZ_ASSERT(mDeleteTimer); + MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel()); + + QM_TRY(MOZ_TO_RESULT(mDeleteTimer->InitWithNamedFuncCallback( + DeleteTimerCallback, this, kDeleteTimeoutMs, nsITimer::TYPE_ONE_SHOT, + "dom::indexeddb::QuotaClient::AsyncDeleteFile"))); + + mPendingDeleteInfos.GetOrInsertNew(aFileManager)->AppendElement(aFileId); + + return NS_OK; +} + +nsresult QuotaClient::FlushPendingFileDeletions() { + AssertIsOnBackgroundThread(); + + QM_TRY(MOZ_TO_RESULT(mDeleteTimer->Cancel())); + + DeleteTimerCallback(mDeleteTimer, this); + + return NS_OK; +} + +nsThreadPool* QuotaClient::GetOrCreateThreadPool() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!IsShuttingDownOnBackgroundThread()); + + 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("IndexedDB Mnt"_ns)); + + mMaintenanceThreadPool = std::move(threadPool); + } + + return mMaintenanceThreadPool; +} + +mozilla::dom::quota::Client::Type QuotaClient::GetType() { + return QuotaClient::IDB; +} + +nsresult QuotaClient::UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT((const auto& [subdirsToProcess, databaseFilenames]), + GetDatabaseFilenames(*aDirectory, + /* aCanceled */ AtomicBool{false})); + + QM_TRY(CollectEachInRange( + subdirsToProcess, + [&databaseFilenames = databaseFilenames, + aDirectory](const nsAString& subdirName) -> Result<Ok, nsresult> { + // If the directory has the correct suffix then it should exist in + // databaseFilenames. + nsDependentSubstring subdirNameBase; + if (GetFilenameBase(subdirName, kFileManagerDirectoryNameSuffix, + subdirNameBase)) { + QM_WARNONLY_TRY(OkIf(databaseFilenames.Contains(subdirNameBase))); + return Ok{}; + } + + // 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. + QM_TRY_INSPECT( + const auto& subdirNameWithSuffix, + ([&databaseFilenames, + &subdirName]() -> Result<nsAutoString, NotOk> { + if (databaseFilenames.Contains(subdirName)) { + return nsAutoString{subdirName + + kFileManagerDirectoryNameSuffix}; + } + + // 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. + const nsAutoString subdirNameWithDot = subdirName + u"."_ns; + QM_TRY(OkIf(databaseFilenames.Contains(subdirNameWithDot)), + Err(NotOk{})); + + return nsAutoString{subdirNameWithDot + + kFileManagerDirectoryNameSuffix}; + }()), + Ok{}); + + // We do have a database that uses this subdir so we should rename it + // now. + QM_TRY_INSPECT(const auto& subdir, + CloneFileAndAppend(*aDirectory, subdirName)); + + DebugOnly<bool> isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(subdir->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory); + + // Check if the subdir with suffix already exists before renaming. + QM_TRY_INSPECT(const auto& subdirWithSuffix, + CloneFileAndAppend(*aDirectory, subdirNameWithSuffix)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(subdirWithSuffix, Exists)); + + if (exists) { + IDB_WARNING("Deleting old %s files directory!", + NS_ConvertUTF16toUTF8(subdirName).get()); + + QM_TRY(MOZ_TO_RESULT(subdir->Remove(/* aRecursive */ true))); + + return Ok{}; + } + + // Finally, rename the subdir. + QM_TRY(MOZ_TO_RESULT(subdir->RenameTo(nullptr, subdirNameWithSuffix))); + + return Ok{}; + })); + + return NS_OK; +} + +nsresult QuotaClient::UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY(CollectEachFile( + *aDirectory, [](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + break; + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); + + // It's reported that files ending with ".tmp" somehow live in the + // indexedDB directories in Bug 1503883. Such files shouldn't exist + // in the indexedDB directory so remove them in this upgrade. + if (StringEndsWith(leafName, u".tmp"_ns)) { + IDB_WARNING("Deleting unknown temporary file!"); + + QM_TRY(MOZ_TO_RESULT(file->Remove(false))); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + })); + + return NS_OK; +} + +Result<UsageInfo, nsresult> QuotaClient::InitOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(this, GetUsageForOriginInternal, + aPersistenceType, aOriginMetadata, + aCanceled, + /* aInitializing*/ true)); +} + +nsresult QuotaClient::InitOriginWithoutTracking( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + return GetUsageForOriginInternal(aPersistenceType, aOriginMetadata, aCanceled, + /* aInitializing*/ true, nullptr); +} + +Result<UsageInfo, nsresult> QuotaClient::GetUsageForOrigin( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(this, GetUsageForOriginInternal, + aPersistenceType, aOriginMetadata, + aCanceled, + /* aInitializing*/ false)); +} + +nsresult QuotaClient::GetUsageForOriginInternal( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled, const bool aInitializing, + UsageInfo* aUsageInfo) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); + + QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& directory, + GetDirectory(aOriginMetadata)); + + // 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. + + // XXX Can we avoid unwrapping into non-const variables here? (Only + // databaseFilenames is currently modified below) + QM_TRY_UNWRAP((auto [subdirsToProcess, databaseFilenames, obsoleteFilenames]), + GetDatabaseFilenames<ObsoleteFilenamesHandling::Include>( + *directory, aCanceled)); + + if (aInitializing) { + QM_TRY(CollectEachInRange( + subdirsToProcess, + [&directory, &obsoleteFilenames = obsoleteFilenames, + &databaseFilenames = databaseFilenames, aPersistenceType, + &aOriginMetadata]( + const nsAString& subdirName) -> Result<Ok, nsresult> { + // The directory must have the correct suffix. + nsDependentSubstring subdirNameBase; + QM_TRY(QM_OR_ELSE_WARN( + // Expression. + ([&subdirName, &subdirNameBase] { + QM_TRY_RETURN(OkIf(GetFilenameBase( + subdirName, kFileManagerDirectoryNameSuffix, + subdirNameBase))); + }()), + // Fallback. + ([&directory, + &subdirName](const NotOk) -> Result<Ok, nsresult> { + // If there is an unexpected directory in the idb + // directory, trying to delete at first instead of + // breaking the whole initialization. + QM_TRY(MOZ_TO_RESULT( + DeleteFilesNoQuota(directory, subdirName)), + Err(NS_ERROR_UNEXPECTED)); + + return Ok{}; + })), + Ok{}); + + if (obsoleteFilenames.Contains(subdirNameBase)) { + // If this fails, it probably means we are in a serious situation. + // e.g. Filesystem corruption. Will handle this in bug 1521541. + QM_TRY(MOZ_TO_RESULT(RemoveDatabaseFilesAndDirectory( + *directory, subdirNameBase, nullptr, aPersistenceType, + aOriginMetadata, u""_ns)), + Err(NS_ERROR_UNEXPECTED)); + + databaseFilenames.Remove(subdirNameBase); + return Ok{}; + } + + // The directory base must exist in databaseFilenames. + // If there is an unexpected directory in the idb directory, trying to + // delete at first instead of breaking the whole initialization. + + // XXX This is still somewhat quirky. It would be nice to make it + // clear that the warning handler is infallible, which would also + // remove the need for the error type conversion. + QM_WARNONLY_TRY(QM_OR_ELSE_WARN( + // Expression. + OkIf(databaseFilenames.Contains(subdirNameBase)) + .mapErr([](const NotOk) { return NS_ERROR_FAILURE; }), + // Fallback. + ([&directory, + &subdirName](const nsresult) -> Result<Ok, nsresult> { + // XXX It seems if we really got here, we can fail the + // MOZ_ASSERT(!quotaManager->IsTemporaryStorageInitialized()); + // assertion in DeleteFilesNoQuota. + QM_TRY(MOZ_TO_RESULT(DeleteFilesNoQuota(directory, subdirName)), + Err(NS_ERROR_UNEXPECTED)); + + return Ok{}; + }))); + + return Ok{}; + })); + } + + for (const auto& databaseFilename : databaseFilenames) { + if (aCanceled) { + break; + } + + QM_TRY_INSPECT( + const auto& fmDirectory, + CloneFileAndAppend(*directory, + databaseFilename + kFileManagerDirectoryNameSuffix)); + + QM_TRY_INSPECT( + const auto& databaseFile, + CloneFileAndAppend(*directory, databaseFilename + kSQLiteSuffix)); + + if (aInitializing) { + QM_TRY(MOZ_TO_RESULT(DatabaseFileManager::InitDirectory( + *fmDirectory, *databaseFile, aOriginMetadata.mOrigin, + TelemetryIdForFile(databaseFile)))); + } + + if (aUsageInfo) { + { + QM_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(databaseFile, GetFileSize)); + + MOZ_ASSERT(fileSize >= 0); + + *aUsageInfo += DatabaseUsageType(Some(uint64_t(fileSize))); + } + + { + QM_TRY_INSPECT(const auto& walFile, + CloneFileAndAppend(*directory, + databaseFilename + kSQLiteWALSuffix)); + + // QM_OR_ELSE_WARN_IF is not used here since we just want to log + // NS_ERROR_FILE_NOT_FOUND result and not spam the reports (the -wal + // file doesn't have to exist). + QM_TRY_INSPECT(const int64_t& walFileSize, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(walFile, GetFileSize), + // Predicate. + ([](const nsresult rv) { + return rv == NS_ERROR_FILE_NOT_FOUND; + }), + // Fallback. + (ErrToOk<0, int64_t>))); + MOZ_ASSERT(walFileSize >= 0); + *aUsageInfo += DatabaseUsageType(Some(uint64_t(walFileSize))); + } + + { + QM_TRY_INSPECT(const auto& fileUsage, + DatabaseFileManager::GetUsage(fmDirectory)); + + *aUsageInfo += fileUsage; + } + } + } + + return NS_OK; +} + +void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + AssertIsOnIOThread(); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->InvalidateFileManagers(aPersistenceType, aOrigin); + } +} + +void QuotaClient::OnRepositoryClearCompleted(PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->InvalidateFileManagers(aPersistenceType); + } +} + +void QuotaClient::ReleaseIOThreadObjects() { + AssertIsOnIOThread(); + + if (IndexedDatabaseManager* mgr = IndexedDatabaseManager::Get()) { + mgr->InvalidateAllFileManagers(); + } +} + +void QuotaClient::AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) { + AssertIsOnBackgroundThread(); + + InvalidateLiveDatabasesMatching([&aDirectoryLockIds](const auto& database) { + // If the database is registered in gLiveDatabaseHashtable then it must have + // a directory lock. + return IsLockForObjectContainedInLockTable(database, aDirectoryLockIds); + }); +} + +void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) { + AssertIsOnBackgroundThread(); + + InvalidateLiveDatabasesMatching([&aContentParentId](const auto& database) { + return database.IsOwnedByProcess(aContentParentId); + }); +} + +void QuotaClient::AbortAllOperations() { + AssertIsOnBackgroundThread(); + + InvalidateLiveDatabasesMatching([](const auto&) { return true; }); +} + +void QuotaClient::StartIdleMaintenance() { + AssertIsOnBackgroundThread(); + if (IsShuttingDownOnBackgroundThread()) { + MOZ_ASSERT(false, "!IsShuttingDownOnBackgroundThread()"); + return; + } + + mBackgroundThread = GetCurrentSerialEventTarget(); + + mMaintenanceQueue.EmplaceBack(MakeRefPtr<Maintenance>(this)); + ProcessMaintenanceQueue(); +} + +void QuotaClient::StopIdleMaintenance() { + AssertIsOnBackgroundThread(); + + if (mCurrentMaintenance) { + mCurrentMaintenance->Abort(); + } + + for (const auto& maintenance : mMaintenanceQueue) { + maintenance->Abort(); + } +} + +void QuotaClient::InitiateShutdown() { + AssertIsOnBackgroundThread(); + + AbortAllOperations(); +} + +bool QuotaClient::IsShutdownCompleted() const { + return (!gFactoryOps || gFactoryOps->IsEmpty()) && + (!gLiveDatabaseHashtable || !gLiveDatabaseHashtable->Count()) && + !mCurrentMaintenance; +} + +void QuotaClient::ForceKillActors() { + // Currently we don't implement force killing actors. +} + +nsCString QuotaClient::GetShutdownStatus() const { + AssertIsOnBackgroundThread(); + + nsCString data; + + if (gFactoryOps && !gFactoryOps->IsEmpty()) { + data.Append("FactoryOperations: "_ns + + IntToCString(static_cast<uint32_t>(gFactoryOps->Length())) + + " ("_ns); + + // XXX It might be confusing to remove duplicates here, as the actual list + // won't match the count then. + nsTHashSet<nsCString> ids; + std::transform(gFactoryOps->cbegin(), gFactoryOps->cend(), + MakeInserter(ids), [](const auto& factoryOp) { + MOZ_ASSERT(factoryOp); + + nsCString id; + factoryOp->Stringify(id); + return id; + }); + + StringJoinAppend(data, ", "_ns, ids); + + data.Append(")\n"); + } + + if (gLiveDatabaseHashtable && gLiveDatabaseHashtable->Count()) { + data.Append("LiveDatabases: "_ns + + IntToCString(gLiveDatabaseHashtable->Count()) + " ("_ns); + + // XXX What's the purpose of adding these to a hashtable before joining them + // to the string? (Maybe this used to be an ordered container before???) + nsTHashSet<nsCString> ids; + + for (const auto& entry : gLiveDatabaseHashtable->Values()) { + MOZ_ASSERT(entry); + + std::transform(entry->mLiveDatabases.cbegin(), + entry->mLiveDatabases.cend(), MakeInserter(ids), + [](const auto& database) { + nsCString id; + database->Stringify(id); + return id; + }); + } + + StringJoinAppend(data, ", "_ns, ids); + + data.Append(")\n"); + } + + if (mCurrentMaintenance) { + data.Append("IdleMaintenance: 1 ("); + mCurrentMaintenance->Stringify(data); + data.Append(")\n"); + } + + return data; +} + +void QuotaClient::FinalizeShutdown() { + RefPtr<ConnectionPool> connectionPool = gConnectionPool.get(); + if (connectionPool) { + connectionPool->Shutdown(); + + gConnectionPool = nullptr; + } + + if (mMaintenanceThreadPool) { + mMaintenanceThreadPool->Shutdown(); + mMaintenanceThreadPool = nullptr; + } + + if (mDeleteTimer) { + MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel()); + mDeleteTimer = nullptr; + } +} + +void QuotaClient::DeleteTimerCallback(nsITimer* aTimer, void* aClosure) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aTimer); + + auto* const self = static_cast<QuotaClient*>(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mDeleteTimer); + MOZ_ASSERT(SameCOMIdentity(self->mDeleteTimer, aTimer)); + + for (const auto& pendingDeleteInfoEntry : self->mPendingDeleteInfos) { + const auto& key = pendingDeleteInfoEntry.GetKey(); + const auto& value = pendingDeleteInfoEntry.GetData(); + MOZ_ASSERT(!value->IsEmpty()); + + RefPtr<DeleteFilesRunnable> runnable = new DeleteFilesRunnable( + SafeRefPtr{key, AcquireStrongRefFromRawPtr{}}, std::move(*value)); + + MOZ_ASSERT(value->IsEmpty()); + + runnable->RunImmediately(); + } + + self->mPendingDeleteInfos.Clear(); +} + +Result<nsCOMPtr<nsIFile>, nsresult> QuotaClient::GetDirectory( + const OriginMetadata& aOriginMetadata) { + QuotaManager* const quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "This should never fail!"); + + QM_TRY_INSPECT(const auto& directory, + quotaManager->GetOriginDirectory(aOriginMetadata)); + + MOZ_ASSERT(directory); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); + + return directory; +} + +template <QuotaClient::ObsoleteFilenamesHandling ObsoleteFilenames> +Result<QuotaClient::GetDatabaseFilenamesResult<ObsoleteFilenames>, nsresult> +QuotaClient::GetDatabaseFilenames(nsIFile& aDirectory, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + GetDatabaseFilenamesResult<ObsoleteFilenames> result; + + QM_TRY(CollectEachFileAtomicCancelable( + aDirectory, aCanceled, + [&result](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, file, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + result.subdirsToProcess.AppendElement(leafName); + break; + + case nsIFileKind::ExistsAsFile: { + if constexpr (ObsoleteFilenames == + ObsoleteFilenamesHandling::Include) { + if (StringBeginsWith(leafName, kIdbDeletionMarkerFilePrefix)) { + result.obsoleteFilenames.Insert( + Substring(leafName, kIdbDeletionMarkerFilePrefix.Length())); + break; + } + } + + // Skip OS metadata files. These files are only used in different + // platforms, but the profile can be shared across different + // operating systems, so we check it on all platforms. + if (QuotaManager::IsOSMetadata(leafName)) { + break; + } + + // Skip files starting with ".". + if (QuotaManager::IsDotFile(leafName)) { + break; + } + + // 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, kSQLiteJournalSuffix) || + StringEndsWith(leafName, kSQLiteSHMSuffix)) { + break; + } + + // The SQLite WAL file does count towards quota, but it is handled + // below once we find the actual database file. + if (StringEndsWith(leafName, kSQLiteWALSuffix)) { + break; + } + + nsDependentSubstring leafNameBase; + if (!GetFilenameBase(leafName, kSQLiteSuffix, leafNameBase)) { + UNKNOWN_FILE_WARNING(leafName); + break; + } + + result.databaseFilenames.Insert(leafNameBase); + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + })); + + return result; +} + +void QuotaClient::ProcessMaintenanceQueue() { + AssertIsOnBackgroundThread(); + + if (mCurrentMaintenance || mMaintenanceQueue.IsEmpty()) { + return; + } + + mCurrentMaintenance = mMaintenanceQueue[0]; + mMaintenanceQueue.RemoveElementAt(0); + + mCurrentMaintenance->RunImmediately(); +} + +/******************************************************************************* + * DeleteFilesRunnable + ******************************************************************************/ + +DeleteFilesRunnable::DeleteFilesRunnable( + SafeRefPtr<DatabaseFileManager> aFileManager, nsTArray<int64_t>&& aFileIds) + : Runnable("dom::indexeddb::DeleteFilesRunnable"), + mOwningEventTarget(GetCurrentSerialEventTarget()), + mFileManager(std::move(aFileManager)), + mFileIds(std::move(aFileIds)), + mState(State_Initial) {} + +void DeleteFilesRunnable::RunImmediately() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_Initial); + + Unused << this->Run(); +} + +void DeleteFilesRunnable::Open() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_Initial); + + QuotaManager* const quotaManager = QuotaManager::Get(); + if (NS_WARN_IF(!quotaManager)) { + Finish(); + return; + } + + RefPtr<DirectoryLock> directoryLock = quotaManager->CreateDirectoryLock( + mFileManager->Type(), mFileManager->OriginMetadata(), quota::Client::IDB, + /* aExclusive */ false); + + mState = State_DirectoryOpenPending; + directoryLock->Acquire(this); +} + +void DeleteFilesRunnable::DoDatabaseWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State_DatabaseWorkOpen); + + if (!mFileManager->Invalidated()) { + for (int64_t fileId : mFileIds) { + if (NS_FAILED(mFileManager->SyncDeleteFile(fileId))) { + NS_WARNING("Failed to delete file!"); + } + } + } + + Finish(); +} + +void DeleteFilesRunnable::Finish() { + MOZ_ASSERT(mState != State_UnblockingOpen); + + // Must set mState before dispatching otherwise we will race with the main + // thread. + mState = State_UnblockingOpen; + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void DeleteFilesRunnable::UnblockOpen() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_UnblockingOpen); + + mDirectoryLock = nullptr; + + mState = State_Completed; +} + +NS_IMPL_ISUPPORTS_INHERITED0(DeleteFilesRunnable, Runnable) + +NS_IMETHODIMP +DeleteFilesRunnable::Run() { + switch (mState) { + case State_Initial: + Open(); + break; + + case State_DatabaseWorkOpen: + DoDatabaseWork(); + break; + + case State_UnblockingOpen: + UnblockOpen(); + break; + + case State_DirectoryOpenPending: + default: + MOZ_CRASH("Should never get here!"); + } + + return NS_OK; +} + +void DeleteFilesRunnable::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO thread + mState = State_DatabaseWorkOpen; + + QM_TRY(MOZ_TO_RESULT( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), + QM_VOID, [this](const nsresult) { Finish(); }); +} + +void DeleteFilesRunnable::DirectoryLockFailed() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + Finish(); +} + +void Maintenance::RegisterDatabaseMaintenance( + DatabaseMaintenance* aDatabaseMaintenance) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabaseMaintenance); + MOZ_ASSERT(mState == State::BeginDatabaseMaintenance); + MOZ_ASSERT( + !mDatabaseMaintenances.Contains(aDatabaseMaintenance->DatabasePath())); + + mDatabaseMaintenances.InsertOrUpdate(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(); +} + +void Maintenance::Stringify(nsACString& aResult) const { + AssertIsOnBackgroundThread(); + + aResult.Append("DatabaseMaintenances: "_ns + + IntToCString(mDatabaseMaintenances.Count()) + " ("_ns); + + // XXX It might be confusing to remove duplicates here, as the actual list + // won't match the count then. + nsTHashSet<nsCString> ids; + std::transform(mDatabaseMaintenances.Values().cbegin(), + mDatabaseMaintenances.Values().cend(), MakeInserter(ids), + [](const auto& entry) { + MOZ_ASSERT(entry); + + nsCString id; + entry->Stringify(id); + + return id; + }); + + StringJoinAppend(aResult, ", "_ns, ids); + + aResult.Append(")"); +} + +nsresult Maintenance::Start() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::Initial); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + 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 (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + IsAborted()) { + return NS_ERROR_ABORT; + } + + IndexedDatabaseManager* const 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 (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsAborted()) { + return NS_ERROR_ABORT; + } + + // Get a shared lock for <profile>/storage/*/*/idb + + mPendingDirectoryLock = QuotaManager::Get()->CreateDirectoryLockInternal( + Nullable<PersistenceType>(), OriginScope::FromNull(), + Nullable<Client::Type>(Client::IDB), + /* aExclusive */ false); + + mState = State::DirectoryOpenPending; + + { + // Pin the directory lock, because Acquire might clear mPendingDirectoryLock + // during the Acquire call. + RefPtr pinnedDirectoryLock = mPendingDirectoryLock; + pinnedDirectoryLock->Acquire(this); + } + + return NS_OK; +} + +nsresult Maintenance::DirectoryOpen() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(mDirectoryLock); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsAborted()) { + return NS_ERROR_ABORT; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + mState = State::DirectoryWorkOpen; + + QM_TRY(MOZ_TO_RESULT( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), + 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 (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + IsAborted()) { + return NS_ERROR_ABORT; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized())); + + // Since idle maintenance may occur before temporary storage is initialized, + // make sure it's initialized here (all non-persistent origins need to be + // cleaned up and quota info needs to be loaded for them). + + // Don't fail whole idle maintenance in case of an error, the persistent + // repository can still + // be processed. + const bool initTemporaryStorageFailed = ["aManager] { + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized()), + true); + return false; + }(); + + const nsCOMPtr<nsIFile> storageDir = + GetFileForPath(quotaManager->GetStoragePath()); + QM_TRY(OkIf(storageDir), NS_ERROR_FAILURE); + + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, Exists)); + + // XXX No warning here? + if (!exists) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, IsDirectory)); + + QM_TRY(OkIf(isDirectory), NS_ERROR_FAILURE); + } + + // There are currently only 4 persistence types, and we want to iterate them + // in this order: + static const PersistenceType kPersistenceTypes[] = { + PERSISTENCE_TYPE_PERSISTENT, PERSISTENCE_TYPE_DEFAULT, + PERSISTENCE_TYPE_TEMPORARY, PERSISTENCE_TYPE_PRIVATE}; + + static_assert( + ArrayLength(kPersistenceTypes) == size_t(PERSISTENCE_TYPE_INVALID), + "Something changed with available persistence types!"); + + constexpr auto idbDirName = + NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME); + + for (const PersistenceType persistenceType : kPersistenceTypes) { + // Loop over "<persistence>" directories. + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + IsAborted()) { + return NS_ERROR_ABORT; + } + + // Don't do any maintenance for private browsing databases, which are only + // temporary. + if (persistenceType == PERSISTENCE_TYPE_PRIVATE) { + continue; + } + + const bool persistent = persistenceType == PERSISTENCE_TYPE_PERSISTENT; + + if (!persistent && initTemporaryStorageFailed) { + // Non-persistent (best effort) repositories can't be processed if + // temporary storage initialization failed. + continue; + } + + // XXX persistenceType == PERSISTENCE_TYPE_PERSISTENT shouldn't be a special + // case... + const auto persistenceTypeString = + persistenceType == PERSISTENCE_TYPE_PERSISTENT + ? "permanent"_ns + : PersistenceTypeToString(persistenceType); + + QM_TRY_INSPECT(const auto& persistenceDir, + CloneFileAndAppend(*storageDir, NS_ConvertASCIItoUTF16( + persistenceTypeString))); + + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(persistenceDir, Exists)); + + if (!exists) { + continue; + } + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(persistenceDir, IsDirectory)); + + if (NS_WARN_IF(!isDirectory)) { + continue; + } + } + + // Loop over "<origin>/idb" directories. + QM_TRY(CollectEachFile( + *persistenceDir, + [this, "aManager, persistent, persistenceType, &idbDirName]( + const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> { + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + IsAborted()) { + return Err(NS_ERROR_ABORT); + } + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsFile: + break; + + case nsIFileKind::ExistsAsDirectory: { + // Get the necessary information about the origin + // (GetOriginMetadata also checks if it's a valid origin). + + QM_TRY_INSPECT(const auto& metadata, + quotaManager->GetOriginMetadata(originDir), + // Not much we can do here... + Ok{}); + + // We now use a dedicated repository for private browsing + // databases, but there could be some forgotten private browsing + // databases in other repositories, so it's better to check for + // that and don't do any maintenance for such databases. + if (metadata.mIsPrivate) { + return Ok{}; + } + + if (persistent) { + // We have to check that all persistent origins are cleaned up, + // but there's no way to do that by one call, we need to + // initialize (and possibly clean up) them one by one + // (EnsureTemporaryStorageIsInitialized cleans up only + // non-persistent origins). + + QM_TRY_UNWRAP( + const DebugOnly<bool> created, + quotaManager->EnsurePersistentOriginIsInitialized(metadata) + .map([](const auto& res) { return res.second; }), + // Not much we can do here... + Ok{}); + + // We found this origin directory by traversing the repository, + // so EnsurePersistentOriginIsInitialized shouldn't report that + // a new directory has been created. + MOZ_ASSERT(!created); + } + + QM_TRY_INSPECT(const auto& idbDir, + CloneFileAndAppend(*originDir, idbDirName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(idbDir, Exists)); + + if (!exists) { + return Ok{}; + } + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(idbDir, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Ok{}); + + nsTArray<nsString> databasePaths; + + // Loop over files in the "idb" directory. + QM_TRY(CollectEachFile( + *idbDir, + [this, &databasePaths](const nsCOMPtr<nsIFile>& idbDirFile) + -> Result<Ok, nsresult> { + if (NS_WARN_IF(QuotaClient:: + IsShuttingDownOnNonBackgroundThread()) || + IsAborted()) { + return Err(NS_ERROR_ABORT); + } + + QM_TRY_UNWRAP(auto idbFilePath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, idbDirFile, GetPath)); + + if (!StringEndsWith(idbFilePath, kSQLiteSuffix)) { + return Ok{}; + } + + QM_TRY_INSPECT(const auto& dirEntryKind, + GetDirEntryKind(*idbDirFile)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + break; + + case nsIFileKind::ExistsAsFile: + // Found a database. + + MOZ_ASSERT(!databasePaths.Contains(idbFilePath)); + + databasePaths.AppendElement(std::move(idbFilePath)); + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while + // iterating. + break; + } + + return Ok{}; + })); + + if (!databasePaths.IsEmpty()) { + mDirectoryInfos.EmplaceBack(persistenceType, metadata, + std::move(databasePaths)); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + })); + } + + 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--) { + CheckedUnsafePtr<FactoryOp>& existingOp = (*gFactoryOps)[index - 1]; + + if (!existingOp->DatabaseFilePathIsKnown()) { + continue; + } + + if (existingOp->DatabaseFilePath() == aDatabasePath) { + return false; + } + } + } + + if (gLiveDatabaseHashtable) { + return std::all_of( + gLiveDatabaseHashtable->Values().cbegin(), + gLiveDatabaseHashtable->Values().cend(), + [&aDatabasePath](const auto& liveDatabasesEntry) { + const auto& liveDatabases = liveDatabasesEntry->mLiveDatabases; + return std::all_of(liveDatabases.cbegin(), liveDatabases.cend(), + [&aDatabasePath](const auto& database) { + return database->FilePath() != aDatabasePath; + }); + }); + } + + return true; + } + }; + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsAborted()) { + return NS_ERROR_ABORT; + } + + RefPtr<nsThreadPool> threadPool; + + for (DirectoryInfo& directoryInfo : mDirectoryInfos) { + RefPtr<DirectoryLock> directoryLock; + + for (const nsAString& databasePath : *directoryInfo.mDatabasePaths) { + if (Helper::IsSafeToRunMaintenance(databasePath)) { + if (!directoryLock) { + directoryLock = mDirectoryLock->SpecializeForClient( + directoryInfo.mPersistenceType, *directoryInfo.mOriginMetadata, + Client::IDB); + MOZ_ASSERT(directoryLock); + } + + // No key needs to be passed here, because we skip encrypted databases + // in DoDirectoryWork as long as they are only used in private browsing + // mode. + const auto databaseMaintenance = MakeRefPtr<DatabaseMaintenance>( + this, directoryLock, directoryInfo.mPersistenceType, + *directoryInfo.mOriginMetadata, databasePath, Nothing{}); + + if (!threadPool) { + threadPool = mQuotaClient->GetOrCreateThreadPool(); + MOZ_ASSERT(threadPool); + } + + // Perform database maintenance on a TaskQueue, as database connections + // require a serial event target when being opened in order to allow + // memory pressure notifications to clear caches (bug 1806751). + const auto taskQueue = TaskQueue::Create( + do_AddRef(threadPool), "IndexedDB Database Maintenance"); + + MOZ_ALWAYS_SUCCEEDS( + taskQueue->Dispatch(databaseMaintenance, NS_DISPATCH_NORMAL)); + + RegisterDatabaseMaintenance(databaseMaintenance); + } + } + } + + mDirectoryInfos.Clear(); + + mDirectoryLock = nullptr; + + if (mDatabaseMaintenances.Count()) { + mState = State::WaitingForDatabaseMaintenancesToComplete; + } else { + mState = State::Finishing; + Finish(); + } + + return NS_OK; +} + +void Maintenance::Finish() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(mState == State::Finishing); + + if (NS_FAILED(mResultCode)) { + nsCString errorName; + GetErrorName(mResultCode, errorName); + + IDB_WARNING("Maintenance finished with error: %s", errorName.get()); + } + + // It can happen that we are only referenced by mCurrentMaintenance which is + // cleared in NoteFinishedMaintenance() + const 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); + + const auto handleError = [this](const nsresult rv) { + if (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)); + } + } + }; + + switch (mState) { + case State::Initial: + QM_TRY(MOZ_TO_RESULT(Start()), NS_OK, handleError); + break; + + case State::CreateIndexedDatabaseManager: + QM_TRY(MOZ_TO_RESULT(CreateIndexedDatabaseManager()), NS_OK, handleError); + break; + + case State::IndexedDatabaseManagerOpen: + QM_TRY(MOZ_TO_RESULT(OpenDirectory()), NS_OK, handleError); + break; + + case State::DirectoryWorkOpen: + QM_TRY(MOZ_TO_RESULT(DirectoryWork()), NS_OK, handleError); + break; + + case State::BeginDatabaseMaintenance: + QM_TRY(MOZ_TO_RESULT(BeginDatabaseMaintenance()), NS_OK, handleError); + break; + + case State::Finishing: + Finish(); + break; + + default: + MOZ_CRASH("Bad state!"); + } + + return NS_OK; +} + +void Maintenance::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = std::exchange(mPendingDirectoryLock, nullptr); + + 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); + + mPendingDirectoryLock = nullptr; + + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + + mState = State::Finishing; + Finish(); +} + +void DatabaseMaintenance::Stringify(nsACString& aResult) const { + AssertIsOnBackgroundThread(); + + aResult.AppendLiteral("Origin:"); + aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin)); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("PersistenceType:"); + aResult.Append(PersistenceTypeToString(mPersistenceType)); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Duration:"); + aResult.AppendInt((PR_Now() - mMaintenance->StartTime()) / PR_USEC_PER_MSEC); +} + +void DatabaseMaintenance::PerformMaintenanceOnDatabase() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mMaintenance); + MOZ_ASSERT(mMaintenance->StartTime()); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mDatabasePath.IsEmpty()); + MOZ_ASSERT(!mOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(!mOriginMetadata.mOrigin.IsEmpty()); + + class MOZ_STACK_CLASS AutoClose final { + NotNull<nsCOMPtr<mozIStorageConnection>> mConnection; + + public: + explicit AutoClose( + MovingNotNull<nsCOMPtr<mozIStorageConnection>> aConnection) + : mConnection(std::move(aConnection)) {} + + ~AutoClose() { MOZ_ALWAYS_SUCCEEDS(mConnection->Close()); } + }; + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + mMaintenance->IsAborted()) { + return; + } + + const nsCOMPtr<nsIFile> databaseFile = GetFileForPath(mDatabasePath); + MOZ_ASSERT(databaseFile); + + QM_TRY_UNWRAP( + const NotNull<nsCOMPtr<mozIStorageConnection>> connection, + GetStorageConnection(*databaseFile, mDirectoryLockId, + TelemetryIdForFile(databaseFile), mMaybeKey), + QM_VOID); + + AutoClose autoClose(connection); + + AutoProgressHandler progressHandler(mMaintenance); + if (NS_WARN_IF(NS_FAILED(progressHandler.Register(connection)))) { + return; + } + + bool databaseIsOk; + nsresult 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; + } + + MaintenanceAction maintenanceAction; + rv = + DetermineMaintenanceAction(*connection, databaseFile, &maintenanceAction); + if (NS_WARN_IF(NS_FAILED(rv))) { + 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(aOk); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + mMaintenance->IsAborted()) { + return NS_ERROR_ABORT; + } + + // First do a full integrity_check. Scope statements tightly here because + // later operations require zero live statements. + { + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, "PRAGMA integrity_check(1);"_ns)); + + QM_TRY_INSPECT(const auto& result, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, *stmt, GetString, 0)); + + QM_TRY(OkIf(result.EqualsLiteral("ok")), NS_OK, + [&aOk](const auto) { *aOk = false; }); + } + + // Now enable and check for foreign key constraints. + { + QM_TRY_INSPECT( + const int32_t& foreignKeysWereEnabled, + ([&aConnection]() -> Result<int32_t, nsresult> { + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, "PRAGMA foreign_keys;"_ns)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); + }())); + + if (!foreignKeysWereEnabled) { + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + } + + QM_TRY_INSPECT(const bool& foreignKeyError, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "PRAGMA foreign_key_check;"_ns)); + + if (!foreignKeysWereEnabled) { + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))); + } + + 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(aDatabaseFile); + MOZ_ASSERT(aMaintenanceAction); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + mMaintenance->IsAborted()) { + return NS_ERROR_ABORT; + } + + QM_TRY_INSPECT(const int32_t& schemaVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + // 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; + } + + // 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); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())) + + // Check to see when we last vacuumed this database. + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, + "SELECT last_vacuum_time, last_vacuum_size " + "FROM database;"_ns)); + + QM_TRY_INSPECT(const PRTime& lastVacuumTime, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 0)); + + QM_TRY_INSPECT(const int64_t& lastVacuumSize, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 1)); + + 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."); + + const 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. + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + "CREATE VIRTUAL TABLE __stats__ USING dbstat;" + "CREATE TEMP TABLE __temp_stats__ AS SELECT * FROM __stats__;"_ns))); + + { // Calculate the percentage of the database pages that are not in + // contiguous order. + QM_TRY_INSPECT( + const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, + "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;"_ns)); + + QM_TRY_INSPECT(const int32_t& percentUnordered, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); + + 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%. + QM_TRY_INSPECT(const int64_t& currentFileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, GetFileSize)); + + 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. + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, "PRAGMA freelist_count;"_ns)); + + QM_TRY_INSPECT(const int32_t& freelistCount, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); + + 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. + QM_TRY_INSPECT( + const auto& stmt, + CreateAndExecuteSingleStepStatement( + aConnection, + "SELECT SUM(unused) * 100.0 / SUM(pgsize) FROM __temp_stats__;"_ns)); + + QM_TRY_INSPECT(const int32_t& percentUnused, + MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt32, 0)); + + 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()); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + mMaintenance->IsAborted()) { + return; + } + + nsresult rv = aConnection.ExecuteSimpleSQL("PRAGMA incremental_vacuum;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void DatabaseMaintenance::FullVacuum(mozIStorageConnection& aConnection, + nsIFile* aDatabaseFile) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aDatabaseFile); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + mMaintenance->IsAborted()) { + return; + } + + QM_WARNONLY_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("VACUUM;"_ns))); + + const PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime > 0); + + QM_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE_MEMBER(aDatabaseFile, GetFileSize)); + + MOZ_ASSERT(fileSize > 0); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT(const auto& stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, + aConnection, CreateStatement, + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByIndex(0, vacuumTime))); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByIndex(1, fileSize))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + return Ok{}; + }())); +} + +void DatabaseMaintenance::RunOnOwningThread() { + AssertIsOnBackgroundThread(); + + mDirectoryLock = nullptr; + + 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( + NotNull<mozIStorageConnection*> aConnection) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + // 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.init(aConnection); + + return NS_OK; +} + +void DatabaseMaintenance::AutoProgressHandler::Unregister() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mConnection); + + nsCOMPtr<mozIStorageProgressHandler> oldHandler; + nsresult rv = + mConnection->get()->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(*mConnection == aConnection); + MOZ_ASSERT(_retval); + + *_retval = QuotaClient::IsShuttingDownOnNonBackgroundThread() || + mMaintenance->IsAborted(); + + return NS_OK; +} + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +// static +nsAutoCString DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( + const Maybe<SerializedKeyRange>& aOptionalKeyRange, + const nsACString& aKeyColumnName) { + return aOptionalKeyRange.isSome() + ? GetBindingClauseForKeyRange(aOptionalKeyRange.ref(), + aKeyColumnName) + : nsAutoCString{}; +} + +// static +nsAutoCString DatabaseOperationBase::GetBindingClauseForKeyRange( + const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName) { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!aKeyColumnName.IsEmpty()); + + constexpr auto andStr = " AND "_ns; + constexpr auto spacecolon = " :"_ns; + + nsAutoCString result; + if (aKeyRange.isOnly()) { + // Both keys equal. + result = + andStr + aKeyColumnName + " ="_ns + spacecolon + kStmtParamNameLowerKey; + } else { + if (!aKeyRange.lower().IsUnset()) { + // Lower key is set. + result.Append(andStr + aKeyColumnName); + result.AppendLiteral(" >"); + if (!aKeyRange.lowerOpen()) { + result.AppendLiteral("="); + } + result.Append(spacecolon + kStmtParamNameLowerKey); + } + + if (!aKeyRange.upper().IsUnset()) { + // Upper key is set. + result.Append(andStr + aKeyColumnName); + result.AppendLiteral(" <"); + if (!aKeyRange.upperOpen()) { + result.AppendLiteral("="); + } + result.Append(spacecolon + kStmtParamNameUpperKey); + } + } + + MOZ_ASSERT(!result.IsEmpty()); + + return result; +} + +// 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 KeyTransformation> +nsresult DatabaseOperationBase::MaybeBindKeyToStatement( + const Key& aKey, mozIStorageStatement* const aStatement, + const nsACString& aParameterName, + const KeyTransformation& aKeyTransformation) { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aStatement); + + if (!aKey.IsUnset()) { + // XXX This case distinction could be avoided if QM_TRY_INSPECT would also + // work with a function not returning a Result<V, E> but simply a V (which + // is const Key& here) and then assuming it is always a success. Or the + // transformation could be changed to return Result<const V&, void> but I + // don't think that Result supports that at the moment. + if constexpr (std::is_reference_v< + std::invoke_result_t<KeyTransformation, Key>>) { + QM_TRY(MOZ_TO_RESULT(aKeyTransformation(aKey).BindToStatement( + aStatement, aParameterName))); + } else { + QM_TRY_INSPECT(const auto& transformedKey, aKeyTransformation(aKey)); + QM_TRY(MOZ_TO_RESULT( + transformedKey.BindToStatement(aStatement, aParameterName))); + } + } + + return NS_OK; +} + +// static +template <typename KeyTransformation> +nsresult DatabaseOperationBase::BindTransformedKeyRangeToStatement( + const SerializedKeyRange& aKeyRange, mozIStorageStatement* const aStatement, + const KeyTransformation& aKeyTransformation) { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aStatement); + + QM_TRY(MOZ_TO_RESULT(MaybeBindKeyToStatement(aKeyRange.lower(), aStatement, + kStmtParamNameLowerKey, + aKeyTransformation))); + + if (aKeyRange.isOnly()) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(MaybeBindKeyToStatement(aKeyRange.upper(), aStatement, + kStmtParamNameUpperKey, + aKeyTransformation))); + + return NS_OK; +} + +// static +nsresult DatabaseOperationBase::BindKeyRangeToStatement( + const SerializedKeyRange& aKeyRange, + mozIStorageStatement* const aStatement) { + return BindTransformedKeyRangeToStatement( + aKeyRange, aStatement, [](const Key& key) -> const auto& { return key; }); +} + +// static +nsresult DatabaseOperationBase::BindKeyRangeToStatement( + const SerializedKeyRange& aKeyRange, mozIStorageStatement* const aStatement, + const nsCString& aLocale) { + MOZ_ASSERT(!aLocale.IsEmpty()); + + return BindTransformedKeyRangeToStatement( + aKeyRange, aStatement, + [&aLocale](const Key& key) { return key.ToLocaleAwareKey(aLocale); }); +} + +// static +void CommonOpenOpHelperBase::AppendConditionClause( + const nsACString& aColumnName, const nsACString& aStatementParameterName, + bool aLessThan, bool aEquals, nsCString& aResult) { + aResult += " AND "_ns + aColumnName + " "_ns; + + if (aLessThan) { + aResult.Append('<'); + } else { + aResult.Append('>'); + } + + if (aEquals) { + aResult.Append('='); + } + + aResult += " :"_ns + aStatementParameterName; +} + +// static +Result<IndexDataValuesAutoArray, nsresult> +DatabaseOperationBase::IndexDataValuesFromUpdateInfos( + const nsTArray<IndexUpdateInfo>& aUpdateInfos, + const UniqueIndexTable& aUniqueIndexTable) { + MOZ_ASSERT_IF(!aUpdateInfos.IsEmpty(), aUniqueIndexTable.Count()); + + AUTO_PROFILER_LABEL("DatabaseOperationBase::IndexDataValuesFromUpdateInfos", + DOM); + + // XXX We could use TransformIntoNewArray here if it allowed to specify that + // an AutoArray should be created. + IndexDataValuesAutoArray indexValues; + + if (NS_WARN_IF(!indexValues.SetCapacity(aUpdateInfos.Length(), fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + std::transform(aUpdateInfos.cbegin(), aUpdateInfos.cend(), + MakeBackInserter(indexValues), + [&aUniqueIndexTable](const IndexUpdateInfo& updateInfo) { + const IndexOrObjectStoreId& indexId = updateInfo.indexId(); + + bool unique = false; + MOZ_ALWAYS_TRUE(aUniqueIndexTable.Get(indexId, &unique)); + + return IndexDataValue{indexId, unique, updateInfo.value(), + updateInfo.localizedValue()}; + }); + indexValues.Sort(); + + return indexValues; +} + +// static +nsresult DatabaseOperationBase::InsertIndexTableRows( + DatabaseConnection* aConnection, const IndexOrObjectStoreId aObjectStoreId, + const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + + AUTO_PROFILER_LABEL("DatabaseOperationBase::InsertIndexTableRows", DOM); + + const uint32_t count = aIndexValues.Length(); + if (!count) { + return NS_OK; + } + + auto insertUniqueStmt = DatabaseConnection::LazyStatement{ + *aConnection, + "INSERT INTO unique_index_data " + "(index_id, value, object_store_id, " + "object_data_key, value_locale) " + "VALUES (:"_ns + + kStmtParamNameIndexId + ", :"_ns + kStmtParamNameValue + ", :"_ns + + kStmtParamNameObjectStoreId + ", :"_ns + kStmtParamNameObjectDataKey + + ", :"_ns + kStmtParamNameValueLocale + ");"_ns}; + auto insertStmt = DatabaseConnection::LazyStatement{ + *aConnection, + "INSERT OR IGNORE INTO index_data " + "(index_id, value, object_data_key, " + "object_store_id, value_locale) " + "VALUES (:"_ns + + kStmtParamNameIndexId + ", :"_ns + kStmtParamNameValue + ", :"_ns + + kStmtParamNameObjectDataKey + ", :"_ns + kStmtParamNameObjectStoreId + + ", :"_ns + kStmtParamNameValueLocale + ");"_ns}; + + for (uint32_t index = 0; index < count; index++) { + const IndexDataValue& info = aIndexValues[index]; + + auto& stmt = info.mUnique ? insertUniqueStmt : insertStmt; + + QM_TRY_INSPECT(const auto& borrowedStmt, stmt.Borrow()); + + QM_TRY(MOZ_TO_RESULT( + borrowedStmt->BindInt64ByName(kStmtParamNameIndexId, info.mIndexId))); + QM_TRY(MOZ_TO_RESULT( + info.mPosition.BindToStatement(&*borrowedStmt, kStmtParamNameValue))); + QM_TRY(MOZ_TO_RESULT(info.mLocaleAwarePosition.BindToStatement( + &*borrowedStmt, kStmtParamNameValueLocale))); + QM_TRY(MOZ_TO_RESULT(borrowedStmt->BindInt64ByName( + kStmtParamNameObjectStoreId, aObjectStoreId))); + QM_TRY(MOZ_TO_RESULT(aObjectStoreKey.BindToStatement( + &*borrowedStmt, kStmtParamNameObjectDataKey))); + + // QM_OR_ELSE_WARN_IF is not used here since we just want to log the + // collision and not spam the reports. + QM_TRY(QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT(borrowedStmt->Execute()), + // Predicate. + ([&info, index, &aIndexValues](nsresult rv) { + 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.mPosition == aIndexValues[index2].mPosition) { + // 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. + return true; + } + } + } + + return false; + }), + // Fallback. + ErrToDefaultOk<>)); + } + + return NS_OK; +} + +// static +nsresult DatabaseOperationBase::DeleteIndexDataTableRows( + DatabaseConnection* aConnection, const Key& aObjectStoreKey, + const nsTArray<IndexDataValue>& aIndexValues) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + + AUTO_PROFILER_LABEL("DatabaseOperationBase::DeleteIndexDataTableRows", DOM); + + const uint32_t count = aIndexValues.Length(); + if (!count) { + return NS_OK; + } + + auto deleteUniqueStmt = DatabaseConnection::LazyStatement{ + *aConnection, "DELETE FROM unique_index_data WHERE index_id = :"_ns + + kStmtParamNameIndexId + " AND value = :"_ns + + kStmtParamNameValue + ";"_ns}; + auto deleteStmt = DatabaseConnection::LazyStatement{ + *aConnection, "DELETE FROM index_data WHERE index_id = :"_ns + + kStmtParamNameIndexId + " AND value = :"_ns + + kStmtParamNameValue + " AND object_data_key = :"_ns + + kStmtParamNameObjectDataKey + ";"_ns}; + + for (uint32_t index = 0; index < count; index++) { + const IndexDataValue& indexValue = aIndexValues[index]; + + auto& stmt = indexValue.mUnique ? deleteUniqueStmt : deleteStmt; + + QM_TRY_INSPECT(const auto& borrowedStmt, stmt.Borrow()); + + QM_TRY(MOZ_TO_RESULT(borrowedStmt->BindInt64ByName(kStmtParamNameIndexId, + indexValue.mIndexId))); + + QM_TRY(MOZ_TO_RESULT(indexValue.mPosition.BindToStatement( + &*borrowedStmt, kStmtParamNameValue))); + + if (!indexValue.mUnique) { + QM_TRY(MOZ_TO_RESULT(aObjectStoreKey.BindToStatement( + &*borrowedStmt, kStmtParamNameObjectDataKey))); + } + + QM_TRY(MOZ_TO_RESULT(borrowedStmt->Execute())); + } + + return NS_OK; +} + +// static +nsresult DatabaseOperationBase::DeleteObjectStoreDataTableRowsWithIndexes( + DatabaseConnection* aConnection, const IndexOrObjectStoreId aObjectStoreId, + const Maybe<SerializedKeyRange>& aKeyRange) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aObjectStoreId); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& hasIndexes, + ObjectStoreHasIndexes(*aConnection, aObjectStoreId), + QM_PROPAGATE, [](const auto&) { MOZ_ASSERT(false); }); + MOZ_ASSERT(hasIndexes, + "Don't use this slow method if there are no indexes!"); + } +#endif + + AUTO_PROFILER_LABEL( + "DatabaseOperationBase::DeleteObjectStoreDataTableRowsWithIndexes", DOM); + + const bool singleRowOnly = aKeyRange.isSome() && aKeyRange.ref().isOnly(); + + Key objectStoreKey; + QM_TRY_INSPECT( + const auto& selectStmt, + ([singleRowOnly, &aConnection, &objectStoreKey, &aKeyRange]() + -> Result<CachingDatabaseConnection::BorrowedStatement, nsresult> { + if (singleRowOnly) { + QM_TRY_UNWRAP(auto selectStmt, + aConnection->BorrowCachedStatement( + "SELECT index_data_values " + "FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + " AND key = :"_ns + + kStmtParamNameKey + ";"_ns)); + + objectStoreKey = aKeyRange.ref().lower(); + + QM_TRY(MOZ_TO_RESULT( + objectStoreKey.BindToStatement(&*selectStmt, kStmtParamNameKey))); + + return selectStmt; + } + + const auto keyRangeClause = + MaybeGetBindingClauseForKeyRange(aKeyRange, kColumnNameKey); + + QM_TRY_UNWRAP( + auto selectStmt, + aConnection->BorrowCachedStatement( + "SELECT index_data_values, "_ns + kColumnNameKey + + " FROM object_data WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + keyRangeClause + ";"_ns)); + + if (aKeyRange.isSome()) { + QM_TRY(MOZ_TO_RESULT( + BindKeyRangeToStatement(aKeyRange.ref(), &*selectStmt))); + } + + return selectStmt; + }())); + + QM_TRY(MOZ_TO_RESULT(selectStmt->BindInt64ByName(kStmtParamNameObjectStoreId, + aObjectStoreId))); + + DebugOnly<uint32_t> resultCountDEBUG = 0; + + QM_TRY(CollectWhileHasResult( + *selectStmt, + [singleRowOnly, aObjectStoreId, &objectStoreKey, &aConnection, + &resultCountDEBUG, indexValues = IndexDataValuesAutoArray{}, + deleteStmt = DatabaseConnection::LazyStatement{ + *aConnection, + "DELETE FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + " AND key = :"_ns + + kStmtParamNameKey + + ";"_ns}](auto& selectStmt) mutable -> Result<Ok, nsresult> { + if (!singleRowOnly) { + QM_TRY( + MOZ_TO_RESULT(objectStoreKey.SetFromStatement(&selectStmt, 1))); + + indexValues.ClearAndRetainStorage(); + } + + QM_TRY(MOZ_TO_RESULT( + ReadCompressedIndexDataValues(selectStmt, 0, indexValues))); + QM_TRY(MOZ_TO_RESULT(DeleteIndexDataTableRows( + aConnection, objectStoreKey, indexValues))); + + QM_TRY_INSPECT(const auto& borrowedDeleteStmt, deleteStmt.Borrow()); + + QM_TRY(MOZ_TO_RESULT(borrowedDeleteStmt->BindInt64ByName( + kStmtParamNameObjectStoreId, aObjectStoreId))); + QM_TRY(MOZ_TO_RESULT(objectStoreKey.BindToStatement( + &*borrowedDeleteStmt, kStmtParamNameKey))); + QM_TRY(MOZ_TO_RESULT(borrowedDeleteStmt->Execute())); + + resultCountDEBUG++; + + return Ok{}; + })); + + MOZ_ASSERT_IF(singleRowOnly, resultCountDEBUG <= 1); + + return NS_OK; +} + +// static +nsresult DatabaseOperationBase::UpdateIndexValues( + DatabaseConnection* aConnection, const IndexOrObjectStoreId aObjectStoreId, + const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + + AUTO_PROFILER_LABEL("DatabaseOperationBase::UpdateIndexValues", DOM); + + QM_TRY_UNWRAP((auto [indexDataValues, indexDataValuesLength]), + MakeCompressedIndexDataValues(aIndexValues)); + + MOZ_ASSERT(!indexDataValuesLength == !(indexDataValues.get())); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "UPDATE object_data SET index_data_values = :"_ns + + kStmtParamNameIndexDataValues + " WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + " AND key = :"_ns + kStmtParamNameKey + + ";"_ns, + [&indexDataValues = indexDataValues, + indexDataValuesLength = indexDataValuesLength, aObjectStoreId, + &aObjectStoreKey]( + mozIStorageStatement& updateStmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + indexDataValues + ? updateStmt.BindAdoptedBlobByName( + kStmtParamNameIndexDataValues, indexDataValues.release(), + indexDataValuesLength) + : updateStmt.BindNullByName(kStmtParamNameIndexDataValues))); + + QM_TRY(MOZ_TO_RESULT(updateStmt.BindInt64ByName( + kStmtParamNameObjectStoreId, aObjectStoreId))); + + QM_TRY(MOZ_TO_RESULT( + aObjectStoreKey.BindToStatement(&updateStmt, kStmtParamNameKey))); + + return Ok{}; + }))); + + return NS_OK; +} + +// static +Result<bool, nsresult> DatabaseOperationBase::ObjectStoreHasIndexes( + DatabaseConnection& aConnection, + const IndexOrObjectStoreId aObjectStoreId) { + aConnection.AssertIsOnConnectionThread(); + MOZ_ASSERT(aObjectStoreId); + + QM_TRY_RETURN(aConnection + .BorrowAndExecuteSingleStepStatement( + "SELECT id " + "FROM object_store_index " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + kOpenLimit + "1;"_ns, + [aObjectStoreId](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( + kStmtParamNameObjectStoreId, aObjectStoreId))); + return Ok{}; + }) + .map(IsSome)); +} + +NS_IMPL_ISUPPORTS_INHERITED(DatabaseOperationBase, Runnable, + mozIStorageProgressHandler) + +NS_IMETHODIMP +DatabaseOperationBase::OnProgress(mozIStorageConnection* aConnection, + bool* _retval) { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(_retval); + + // This is intentionally racy. + *_retval = QuotaClient::IsShuttingDownOnNonBackgroundThread() || + !OperationMayProceed(); + return NS_OK; +} + +DatabaseOperationBase::AutoSetProgressHandler::AutoSetProgressHandler() + : mConnection(Nothing()) +#ifdef DEBUG + , + mDEBUGDatabaseOp(nullptr) +#endif +{ + MOZ_ASSERT(!IsOnBackgroundThread()); +} + +DatabaseOperationBase::AutoSetProgressHandler::~AutoSetProgressHandler() { + MOZ_ASSERT(!IsOnBackgroundThread()); + + if (mConnection) { + Unregister(); + } +} + +nsresult DatabaseOperationBase::AutoSetProgressHandler::Register( + mozIStorageConnection& aConnection, DatabaseOperationBase* aDatabaseOp) { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aDatabaseOp); + MOZ_ASSERT(!mConnection); + + QM_TRY_UNWRAP( + const DebugOnly oldProgressHandler, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageProgressHandler>, aConnection, SetProgressHandler, + kStorageProgressGranularity, aDatabaseOp)); + + MOZ_ASSERT(!oldProgressHandler.inspect()); + + mConnection = SomeRef(aConnection); +#ifdef DEBUG + mDEBUGDatabaseOp = aDatabaseOp; +#endif + + return NS_OK; +} + +void DatabaseOperationBase::AutoSetProgressHandler::Unregister() { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(mConnection); + + nsCOMPtr<mozIStorageProgressHandler> oldHandler; + MOZ_ALWAYS_SUCCEEDS( + mConnection->RemoveProgressHandler(getter_AddRefs(oldHandler))); + MOZ_ASSERT(oldHandler == mDEBUGDatabaseOp); + + mConnection = Nothing(); +} + +FactoryOp::FactoryOp(SafeRefPtr<Factory> aFactory, + RefPtr<ThreadsafeContentParentHandle> aContentHandle, + const CommonFactoryRequestParams& aCommonParams, + bool aDeleting) + : DatabaseOperationBase(aFactory->GetLoggingInfo()->Id(), + aFactory->GetLoggingInfo()->NextRequestSN()), + mFactory(std::move(aFactory)), + mContentHandle(std::move(aContentHandle)), + mCommonParams(aCommonParams), + mDirectoryLockId(-1), + mState(State::Initial), + mWaitingForPermissionRetry(false), + mEnforcingQuota(true), + mDeleting(aDeleting), + mChromeWriteAccessAllowed(false) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mFactory); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); +} + +void FactoryOp::NoteDatabaseBlocked(Database* aDatabase) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabase); + 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 (auto& info : mMaybeBlockedDatabases) { + 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(); + } +} + +void FactoryOp::NoteDatabaseClosed(Database* const aDatabase) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); + MOZ_ASSERT(!mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(mMaybeBlockedDatabases.Contains(aDatabase)); + + mMaybeBlockedDatabases.RemoveElement(aDatabase); + + if (!mMaybeBlockedDatabases.IsEmpty()) { + return; + } + + DatabaseActorInfo* info; + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); + MOZ_ASSERT(info->mWaitingFactoryOp == this); + + if (AreActorsAlive()) { + // The IPDL strong reference has not yet been released, so we can clear + // mWaitingFactoryOp immediately. + info->mWaitingFactoryOp = nullptr; + + WaitForTransactions(); + return; + } + + // The IPDL strong reference has been released, mWaitingFactoryOp holds the + // last strong reference to us, so we need to move it to a stack variable + // instead of clearing it immediately (We could clear it immediately if only + // the other actor is destroyed, but we don't need to optimize for that, and + // move it anyway). + const RefPtr<FactoryOp> waitingFactoryOp = std::move(info->mWaitingFactoryOp); + + IDB_REPORT_INTERNAL_ERR(); + SetFailureCodeIfUnset(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + + // We hold a strong ref in waitingFactoryOp, so it's safe to call Run() + // directly. + + mState = State::SendingResults; + MOZ_ALWAYS_SUCCEEDS(Run()); +} + +void FactoryOp::StringifyState(nsACString& aResult) const { + AssertIsOnOwningThread(); + + switch (mState) { + case State::Initial: + aResult.AppendLiteral("Initial"); + return; + + case State::FinishOpen: + aResult.AppendLiteral("FinishOpen"); + return; + + case State::DirectoryOpenPending: + aResult.AppendLiteral("DirectoryOpenPending"); + return; + + case State::DatabaseOpenPending: + aResult.AppendLiteral("DatabaseOpenPending"); + return; + + case State::DatabaseWorkOpen: + aResult.AppendLiteral("DatabaseWorkOpen"); + return; + + case State::BeginVersionChange: + aResult.AppendLiteral("BeginVersionChange"); + return; + + case State::WaitingForOtherDatabasesToClose: + aResult.AppendLiteral("WaitingForOtherDatabasesToClose"); + return; + + case State::WaitingForTransactionsToComplete: + aResult.AppendLiteral("WaitingForTransactionsToComplete"); + return; + + case State::DatabaseWorkVersionChange: + aResult.AppendLiteral("DatabaseWorkVersionChange"); + return; + + case State::SendingResults: + aResult.AppendLiteral("SendingResults"); + return; + + case State::Completed: + aResult.AppendLiteral("Completed"); + return; + + default: + MOZ_CRASH("Bad state!"); + } +} + +void FactoryOp::Stringify(nsACString& aResult) const { + AssertIsOnOwningThread(); + + aResult.AppendLiteral("PersistenceType:"); + aResult.Append( + PersistenceTypeToString(mCommonParams.metadata().persistenceType())); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("Origin:"); + aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin)); + aResult.Append(kQuotaGenericDelimiter); + + aResult.AppendLiteral("State:"); + StringifyState(aResult); +} + +nsresult FactoryOp::Open() { + AssertIsOnMainThread(); + MOZ_ASSERT(mState == State::Initial); + + RefPtr<ContentParent> contentParent = + mContentHandle ? mContentHandle->GetContentParent() : nullptr; + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + QM_TRY_INSPECT(const auto& permission, CheckPermission(contentParent)); + + MOZ_ASSERT(permission == PermissionValue::kPermissionAllowed || + permission == PermissionValue::kPermissionDenied); + + if (permission == PermissionValue::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; + } + } + + MOZ_ASSERT(permission == PermissionValue::kPermissionAllowed); + + mState = State::FinishOpen; + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +nsresult FactoryOp::DirectoryOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(gFactoryOps); + + // See if this FactoryOp needs to wait. + const bool delayed = + std::any_of( + gFactoryOps->rbegin(), gFactoryOps->rend(), + [foundThis = false, &self = *this](const auto& existingOp) mutable { + if (existingOp == &self) { + foundThis = true; + return false; + } + + if (foundThis && self.MustWaitFor(*existingOp)) { + // Only one op can be delayed. + MOZ_ASSERT(!existingOp->mDelayedOp); + existingOp->mDelayedOp = &self; + return true; + } + + return false; + }) || + [&self = *this] { + QuotaClient* quotaClient = QuotaClient::GetInstance(); + MOZ_ASSERT(quotaClient); + + if (RefPtr<Maintenance> currentMaintenance = + quotaClient->GetCurrentMaintenance()) { + if (RefPtr<DatabaseMaintenance> databaseMaintenance = + currentMaintenance->GetDatabaseMaintenance( + self.mDatabaseFilePath)) { + databaseMaintenance->WaitForCompletion(&self); + return true; + } + } + + return false; + }(); + + mState = State::DatabaseOpenPending; + if (!delayed) { + QM_TRY(MOZ_TO_RESULT(DatabaseOpen())); + } + + 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* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO thread. + mState = State::DatabaseWorkOpen; + + QM_TRY(MOZ_TO_RESULT( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), + NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + 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::CleanupMetadata() { + AssertIsOnOwningThread(); + + if (mDelayedOp) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); + } + + MOZ_ASSERT(gFactoryOps); + gFactoryOps->RemoveElement(this); + + // We might get here even after QuotaManagerOpen failed, so we need to check + // if we have a quota manager. + quota::QuotaManager::SafeMaybeRecordQuotaClientShutdownStep( + quota::Client::IDB, "An element was removed from gFactoryOps"_ns); + + // Match the IncreaseBusyCount in AllocPBackgroundIDBFactoryRequestParent(). + DecreaseBusyCount(); +} + +void FactoryOp::FinishSendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(mFactory); + + mState = State::Completed; + + // Make sure to release the factory on this thread. + mFactory = nullptr; +} + +Result<PermissionValue, nsresult> FactoryOp::CheckPermission( + ContentParent* aContentParent) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mState == State::Initial); + + 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 Err(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + } + + const ContentPrincipalInfo& contentPrincipalInfo = + principalInfo.get_ContentPrincipalInfo(); + if (contentPrincipalInfo.attrs().mPrivateBrowsingId != 0) { + if (StaticPrefs::dom_indexedDB_privateBrowsing_enabled()) { + // Explicitly disallow moz-extension urls from using the encrypted + // indexedDB storage mode when the caller is an extension (see Bug + // 1841806). + if (StringBeginsWith(contentPrincipalInfo.originNoSuffix(), + "moz-extension:"_ns)) { + return Err(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + } + + // XXX Not sure if this should be done from here, it goes beyond + // checking the permissions. + mInPrivateBrowsing.Flip(); + } else { + return Err(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + } + } + } + + 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_ConvertUTF16toUTF8 databaseName(mCommonParams.metadata().name()); + + const nsAutoCString permissionStringWrite = + kPermissionStringBase + databaseName + kPermissionWriteSuffix; + const nsAutoCString permissionStringRead = + kPermissionStringBase + databaseName + kPermissionReadSuffix; + + 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 Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + // Opening or deleting requires read permissions. + if (!canRead) { + aContentParent->KillHard("IndexedDB CheckPermission 3"); + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + mChromeWriteAccessAllowed = canWrite; + } else { + mChromeWriteAccessAllowed = true; + } + + return PermissionValue::kPermissionAllowed; + } + + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + QM_TRY_INSPECT( + const auto& permission, + ([persistenceType, + origin = QuotaManager::GetOriginFromValidatedPrincipalInfo( + principalInfo)]() -> mozilla::Result<PermissionValue, nsresult> { + if (persistenceType == PERSISTENCE_TYPE_PERSISTENT) { + if (QuotaManager::IsOriginInternal(origin)) { + return PermissionValue::kPermissionAllowed; + } + return Err(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + } + return PermissionValue::kPermissionAllowed; + })()); + + return permission; +} + +nsresult FactoryOp::SendVersionChangeMessages( + DatabaseActorInfo* aDatabaseActorInfo, Maybe<Database&> aOpeningDatabase, + uint64_t aOldVersion, const Maybe<uint64_t>& 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) { + nsTArray<MaybeBlockedDatabaseInfo> maybeBlockedDatabases; + for (const auto& database : aDatabaseActorInfo->mLiveDatabases) { + if ((!aOpeningDatabase || database.get() != &aOpeningDatabase.ref()) && + !database->IsClosed() && + NS_WARN_IF(!maybeBlockedDatabases.AppendElement( + SafeRefPtr{database.get(), AcquireStrongRefFromRawPtr{}}, + fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + mMaybeBlockedDatabases = std::move(maybeBlockedDatabases); + } + + // We don't want to wait forever if we were not able to send the + // message. + mMaybeBlockedDatabases.RemoveLastElements( + mMaybeBlockedDatabases.end() - + std::remove_if(mMaybeBlockedDatabases.begin(), + mMaybeBlockedDatabases.end(), + [aOldVersion, &aNewVersion](auto& maybeBlockedDatabase) { + return !maybeBlockedDatabase->SendVersionChange( + aOldVersion, aNewVersion); + })); + + return NS_OK; +} // namespace indexedDB + +// 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(mOriginMetadata.mOrigin.IsEmpty()); + MOZ_ASSERT(!mDirectoryLock); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + QM_TRY(QuotaManager::EnsureCreated()); + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); + + const DatabaseMetadata& metadata = mCommonParams.metadata(); + + const PersistenceType persistenceType = metadata.persistenceType(); + + if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + mOriginMetadata = {QuotaManager::GetInfoForChrome(), persistenceType}; + + MOZ_ASSERT(QuotaManager::IsOriginInternal(mOriginMetadata.mOrigin)); + + mEnforcingQuota = false; + } else { + MOZ_ASSERT(principalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + QM_TRY_UNWRAP( + auto principalMetadata, + quotaManager->GetInfoFromValidatedPrincipalInfo(principalInfo)); + + mOriginMetadata = {std::move(principalMetadata), persistenceType}; + + mEnforcingQuota = persistenceType != PERSISTENCE_TYPE_PERSISTENT; + } + + QuotaManager::GetStorageId(persistenceType, mOriginMetadata.mOrigin, + Client::IDB, mDatabaseId); + + mDatabaseId.Append('*'); + mDatabaseId.Append(NS_ConvertUTF16toUTF8(metadata.name())); + + // Need to get database file path before opening the directory. + // XXX: For what reason? + QM_TRY_UNWRAP( + mDatabaseFilePath, + ([this, metadata, quotaManager]() -> mozilla::Result<nsString, nsresult> { + QM_TRY_INSPECT(const auto& dbFile, + quotaManager->GetOriginDirectory(mOriginMetadata)); + + QM_TRY(MOZ_TO_RESULT(dbFile->Append( + NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); + + QM_TRY(MOZ_TO_RESULT( + dbFile->Append(GetDatabaseFilenameBase(metadata.name(), + mOriginMetadata.mIsPrivate) + + kSQLiteSuffix))); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); + }())); + + // Open directory + RefPtr<DirectoryLock> directoryLock = quotaManager->CreateDirectoryLock( + persistenceType, mOriginMetadata, Client::IDB, + /* aExclusive */ false); + + mState = State::DirectoryOpenPending; + + directoryLock->Acquire(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.mOriginMetadata.mOrigin == mOriginMetadata.mOrigin && + aExistingOp.mDatabaseId == mDatabaseId; +} + +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() { + const auto handleError = [this](const nsresult rv) { + if (mState != State::SendingResults) { + SetFailureCodeIfUnset(rv); + + // Must set mState before dispatching otherwise we will race with the + // owning thread. + mState = State::SendingResults; + + if (IsOnOwningThread()) { + SendResults(); + } else { + MOZ_ALWAYS_SUCCEEDS( + mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + } + } + }; + + switch (mState) { + case State::Initial: + QM_WARNONLY_TRY(MOZ_TO_RESULT(Open()), handleError); + break; + + case State::FinishOpen: + QM_WARNONLY_TRY(MOZ_TO_RESULT(FinishOpen()), handleError); + break; + + case State::DatabaseOpenPending: + QM_WARNONLY_TRY(MOZ_TO_RESULT(DatabaseOpen()), handleError); + break; + + case State::DatabaseWorkOpen: + QM_WARNONLY_TRY(MOZ_TO_RESULT(DoDatabaseWork()), handleError); + break; + + case State::BeginVersionChange: + QM_WARNONLY_TRY(MOZ_TO_RESULT(BeginVersionChange()), handleError); + break; + + case State::WaitingForTransactionsToComplete: + QM_WARNONLY_TRY(MOZ_TO_RESULT(DispatchToWorkThread()), handleError); + break; + + case State::SendingResults: + SendResults(); + break; + + default: + MOZ_CRASH("Bad state!"); + } + + return NS_OK; +} + +void FactoryOp::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLock); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + MOZ_ASSERT(mDirectoryLock->Id() >= 0); + mDirectoryLockId = mDirectoryLock->Id(); + + QM_WARNONLY_TRY(MOZ_TO_RESULT(DirectoryOpen()), [this](const nsresult rv) { + SetFailureCodeIfUnset(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()); + }); +} + +void FactoryOp::DirectoryLockFailed() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + if (!HasFailed()) { + IDB_REPORT_INTERNAL_ERR(); + SetFailureCode(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(); +} + +OpenDatabaseOp::OpenDatabaseOp( + SafeRefPtr<Factory> aFactory, + RefPtr<ThreadsafeContentParentHandle> aContentHandle, + const CommonFactoryRequestParams& aParams) + : FactoryOp(std::move(aFactory), std::move(aContentHandle), aParams, + /* aDeleting */ false), + mMetadata(MakeSafeRefPtr<FullDatabaseMetadata>(aParams.metadata())), + mRequestedVersion(aParams.metadata().version()), + mVersionChangeOp(nullptr), + mTelemetryId(0) {} + +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); + + AUTO_PROFILER_LABEL("OpenDatabaseOp::DoDatabaseWork", DOM); + + QM_TRY(OkIf(!QuotaClient::IsShuttingDownOnNonBackgroundThread()), + NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + if (!OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const nsAString& databaseName = mCommonParams.metadata().name(); + const PersistenceType persistenceType = + mCommonParams.metadata().persistenceType(); + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized())); + + QM_TRY_INSPECT( + const auto& dbDirectory, + ([persistenceType, "aManager, this]() + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + if (persistenceType == PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY_RETURN(quotaManager->EnsurePersistentOriginIsInitialized( + mOriginMetadata)); + } + + QM_TRY( + MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized())); + QM_TRY_RETURN(quotaManager->EnsureTemporaryOriginIsInitialized( + persistenceType, mOriginMetadata)); + }() + .map([](const auto& res) { return res.first; }))); + + QM_TRY(MOZ_TO_RESULT( + dbDirectory->Append(NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); + + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(dbDirectory, Exists)); + + if (!exists) { + QM_TRY(MOZ_TO_RESULT(dbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755))); + } +#ifdef DEBUG + else { + bool isDirectory; + MOZ_ASSERT(NS_SUCCEEDED(dbDirectory->IsDirectory(&isDirectory))); + MOZ_ASSERT(isDirectory); + } +#endif + } + + const auto databaseFilenameBase = + GetDatabaseFilenameBase(databaseName, mOriginMetadata.mIsPrivate); + + QM_TRY_INSPECT(const auto& markerFile, + CloneFileAndAppend(*dbDirectory, kIdbDeletionMarkerFilePrefix + + databaseFilenameBase)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(markerFile, Exists)); + + if (exists) { + // Delete the database and directroy since they should be deleted in + // previous operation. + // Note: only update usage to the QuotaManager when mEnforcingQuota == true + QM_TRY(MOZ_TO_RESULT(RemoveDatabaseFilesAndDirectory( + *dbDirectory, databaseFilenameBase, + mEnforcingQuota ? quotaManager : nullptr, persistenceType, + mOriginMetadata, databaseName))); + } + + QM_TRY_INSPECT( + const auto& dbFile, + CloneFileAndAppend(*dbDirectory, databaseFilenameBase + kSQLiteSuffix)); + + mTelemetryId = TelemetryIdForFile(dbFile); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const auto& databaseFilePath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); + + MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); + } +#endif + + QM_TRY_INSPECT( + const auto& fmDirectory, + CloneFileAndAppend(*dbDirectory, databaseFilenameBase + + kFileManagerDirectoryNameSuffix)); + + IndexedDatabaseManager* const idm = IndexedDatabaseManager::Get(); + MOZ_ASSERT(idm); + + SafeRefPtr<DatabaseFileManager> fileManager = idm->GetFileManager( + persistenceType, mOriginMetadata.mOrigin, databaseName); + + if (!fileManager) { + fileManager = MakeSafeRefPtr<DatabaseFileManager>( + persistenceType, mOriginMetadata, databaseName, mDatabaseId, + mEnforcingQuota, mInPrivateBrowsing); + } + + Maybe<const CipherKey> maybeKey = + mInPrivateBrowsing + ? Some(fileManager->MutableCipherKeyManagerRef().Ensure()) + : Nothing(); + + MOZ_RELEASE_ASSERT(mInPrivateBrowsing == maybeKey.isSome()); + + QM_TRY_UNWRAP( + NotNull<nsCOMPtr<mozIStorageConnection>> connection, + CreateStorageConnection(*dbFile, *fmDirectory, databaseName, + mOriginMetadata.mOrigin, mDirectoryLockId, + mTelemetryId, maybeKey)); + + AutoSetProgressHandler asph; + QM_TRY(MOZ_TO_RESULT(asph.Register(*connection, this))); + + QM_TRY(MOZ_TO_RESULT(LoadDatabaseInformation(*connection))); + + 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. + // Otherwise, treat it as if the current version were requested. + mRequestedVersion = mMetadata->mCommonMetadata.version() == 0 + ? 1 + : mMetadata->mCommonMetadata.version(); + } + + QM_TRY(OkIf(mMetadata->mCommonMetadata.version() <= mRequestedVersion), + NS_ERROR_DOM_INDEXEDDB_VERSION_ERR); + + if (!fileManager->Initialized()) { + QM_TRY(MOZ_TO_RESULT(fileManager->Init(fmDirectory, *connection))); + + idm->AddFileManager(fileManager.clonePtr()); + } + + mFileManager = std::move(fileManager); + + // Must close connection before dispatching otherwise we might race with the + // connection thread which needs to open the same database. + asph.Unregister(); + + MOZ_ALWAYS_SUCCEEDS(connection->Close()); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + mState = (mMetadata->mCommonMetadata.version() == mRequestedVersion) + ? State::SendingResults + : State::BeginVersionChange; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult OpenDatabaseOp::LoadDatabaseInformation( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(mMetadata); + + { + // Load version information. + QM_TRY_INSPECT( + const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT name, origin, version FROM database"_ns)); + + QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED); + + QM_TRY_INSPECT(const auto& databaseName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, stmt, GetString, 0)); + + QM_TRY(OkIf(mCommonParams.metadata().name() == databaseName), + NS_ERROR_FILE_CORRUPTED); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, stmt, GetUTF8String, 1)); + + // We can't just compare these strings directly. See bug 1339081 comment 69. + QM_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(mOriginMetadata.mOrigin, + origin)), + NS_ERROR_FILE_CORRUPTED); + + QM_TRY_INSPECT(const int64_t& version, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 2)); + + mMetadata->mCommonMetadata.version() = uint64_t(version); + } + + ObjectStoreTable& objectStores = mMetadata->mObjectStores; + + QM_TRY_INSPECT( + const auto& lastObjectStoreId, + ([&aConnection, + &objectStores]() -> mozilla::Result<IndexOrObjectStoreId, nsresult> { + // Load object store names and ids. + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "SELECT id, auto_increment, name, key_path " + "FROM object_store"_ns)); + + IndexOrObjectStoreId lastObjectStoreId = 0; + + QM_TRY(CollectWhileHasResult( + *stmt, + [&lastObjectStoreId, &objectStores, + usedIds = Maybe<nsTHashSet<uint64_t>>{}, + usedNames = Maybe<nsTHashSet<nsString>>{}]( + auto& stmt) mutable -> mozilla::Result<Ok, nsresult> { + QM_TRY_INSPECT(const IndexOrObjectStoreId& objectStoreId, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0)); + + if (!usedIds) { + usedIds.emplace(); + } + + QM_TRY(OkIf(objectStoreId > 0), Err(NS_ERROR_FILE_CORRUPTED)); + QM_TRY(OkIf(!usedIds.ref().Contains(objectStoreId)), + Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY(OkIf(usedIds.ref().Insert(objectStoreId, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + nsString name; + QM_TRY(MOZ_TO_RESULT(stmt.GetString(2, name))); + + if (!usedNames) { + usedNames.emplace(); + } + + QM_TRY(OkIf(!usedNames.ref().Contains(name)), + Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY(OkIf(usedNames.ref().Insert(name, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + ObjectStoreMetadata commonMetadata; + commonMetadata.id() = objectStoreId; + commonMetadata.name() = std::move(name); + + QM_TRY_INSPECT( + const int32_t& columnType, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetTypeOfIndex, 3)); + + if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) { + commonMetadata.keyPath() = KeyPath(0); + } else { + MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_TEXT); + + nsString keyPathSerialization; + QM_TRY(MOZ_TO_RESULT(stmt.GetString(3, keyPathSerialization))); + + commonMetadata.keyPath() = + KeyPath::DeserializeFromString(keyPathSerialization); + QM_TRY(OkIf(commonMetadata.keyPath().IsValid()), + Err(NS_ERROR_FILE_CORRUPTED)); + } + + QM_TRY_INSPECT(const int64_t& nextAutoIncrementId, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 1)); + + commonMetadata.autoIncrement() = !!nextAutoIncrementId; + + QM_TRY(OkIf(objectStores.InsertOrUpdate( + objectStoreId, + MakeSafeRefPtr<FullObjectStoreMetadata>( + std::move(commonMetadata), + FullObjectStoreMetadata::AutoIncrementIds{ + nextAutoIncrementId, nextAutoIncrementId}), + fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + lastObjectStoreId = std::max(lastObjectStoreId, objectStoreId); + + return Ok{}; + })); + + return lastObjectStoreId; + }())); + + QM_TRY_INSPECT( + const auto& lastIndexId, + ([&aConnection, + &objectStores]() -> mozilla::Result<IndexOrObjectStoreId, nsresult> { + // Load index information + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "SELECT " + "id, object_store_id, name, key_path, " + "unique_index, multientry, " + "locale, is_auto_locale " + "FROM object_store_index"_ns)); + + IndexOrObjectStoreId lastIndexId = 0; + + QM_TRY(CollectWhileHasResult( + *stmt, + [&lastIndexId, &objectStores, &aConnection, + usedIds = Maybe<nsTHashSet<uint64_t>>{}, + usedNames = Maybe<nsTHashSet<nsString>>{}]( + auto& stmt) mutable -> mozilla::Result<Ok, nsresult> { + QM_TRY_INSPECT(const IndexOrObjectStoreId& objectStoreId, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 1)); + + // XXX Why does this return NS_ERROR_OUT_OF_MEMORY if we don't + // know the object store id? + + auto objectStoreMetadata = objectStores.Lookup(objectStoreId); + QM_TRY(OkIf(static_cast<bool>(objectStoreMetadata)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + MOZ_ASSERT((*objectStoreMetadata)->mCommonMetadata.id() == + objectStoreId); + + IndexOrObjectStoreId indexId; + QM_TRY(MOZ_TO_RESULT(stmt.GetInt64(0, &indexId))); + + if (!usedIds) { + usedIds.emplace(); + } + + QM_TRY(OkIf(indexId > 0), Err(NS_ERROR_FILE_CORRUPTED)); + QM_TRY(OkIf(!usedIds.ref().Contains(indexId)), + Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY(OkIf(usedIds.ref().Insert(indexId, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + nsString name; + QM_TRY(MOZ_TO_RESULT(stmt.GetString(2, name))); + + const nsAutoString hashName = + IntToString(indexId) + u":"_ns + name; + + if (!usedNames) { + usedNames.emplace(); + } + + QM_TRY(OkIf(!usedNames.ref().Contains(hashName)), + Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY(OkIf(usedNames.ref().Insert(hashName, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + auto indexMetadata = MakeSafeRefPtr<FullIndexMetadata>(); + indexMetadata->mCommonMetadata.id() = indexId; + indexMetadata->mCommonMetadata.name() = name; + +#ifdef DEBUG + { + int32_t columnType; + nsresult rv = stmt.GetTypeOfIndex(3, &columnType); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(columnType != mozIStorageStatement::VALUE_TYPE_NULL); + } +#endif + + nsString keyPathSerialization; + QM_TRY(MOZ_TO_RESULT(stmt.GetString(3, keyPathSerialization))); + + indexMetadata->mCommonMetadata.keyPath() = + KeyPath::DeserializeFromString(keyPathSerialization); + QM_TRY(OkIf(indexMetadata->mCommonMetadata.keyPath().IsValid()), + Err(NS_ERROR_FILE_CORRUPTED)); + + int32_t scratch; + QM_TRY(MOZ_TO_RESULT(stmt.GetInt32(4, &scratch))); + + indexMetadata->mCommonMetadata.unique() = !!scratch; + + QM_TRY(MOZ_TO_RESULT(stmt.GetInt32(5, &scratch))); + + indexMetadata->mCommonMetadata.multiEntry() = !!scratch; + + const bool localeAware = !stmt.IsNull(6); + if (localeAware) { + QM_TRY(MOZ_TO_RESULT(stmt.GetUTF8String( + 6, indexMetadata->mCommonMetadata.locale()))); + + QM_TRY(MOZ_TO_RESULT(stmt.GetInt32(7, &scratch))); + + indexMetadata->mCommonMetadata.autoLocale() = !!scratch; + + // Update locale-aware indexes if necessary + const nsCString& indexedLocale = + indexMetadata->mCommonMetadata.locale(); + const bool& isAutoLocale = + indexMetadata->mCommonMetadata.autoLocale(); + const nsCString& systemLocale = + IndexedDatabaseManager::GetLocale(); + if (!systemLocale.IsEmpty() && isAutoLocale && + !indexedLocale.Equals(systemLocale)) { + QM_TRY(MOZ_TO_RESULT(UpdateLocaleAwareIndex( + aConnection, indexMetadata->mCommonMetadata, + systemLocale))); + } + } + + QM_TRY(OkIf((*objectStoreMetadata) + ->mIndexes.InsertOrUpdate( + indexId, std::move(indexMetadata), fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + lastIndexId = std::max(lastIndexId, indexId); + + return Ok{}; + })); + + return lastIndexId; + }())); + + QM_TRY(OkIf(lastObjectStoreId != INT64_MAX), + NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); + QM_TRY(OkIf(lastIndexId != INT64_MAX), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + mMetadata->mNextObjectStoreId = lastObjectStoreId + 1; + mMetadata->mNextIndexId = lastIndexId + 1; + + return NS_OK; +} + +/* static */ +nsresult OpenDatabaseOp::UpdateLocaleAwareIndex( + mozIStorageConnection& aConnection, const IndexMetadata& aIndexMetadata, + const nsCString& aLocale) { + const auto indexTable = + aIndexMetadata.unique() ? "unique_index_data"_ns : "index_data"_ns; + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + const nsCString readQuery = "SELECT value, object_data_key FROM "_ns + + indexTable + " WHERE index_id = :index_id"_ns; + + QM_TRY_INSPECT(const auto& readStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, readQuery)); + + QM_TRY(MOZ_TO_RESULT(readStmt->BindInt64ByIndex(0, aIndexMetadata.id()))); + + QM_TRY(CollectWhileHasResult( + *readStmt, + [&aConnection, &indexTable, &aIndexMetadata, &aLocale, + writeStmt = nsCOMPtr<mozIStorageStatement>{}]( + auto& readStmt) mutable -> mozilla::Result<Ok, nsresult> { + if (!writeStmt) { + QM_TRY_UNWRAP( + writeStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "UPDATE "_ns + indexTable + "SET value_locale = :"_ns + + kStmtParamNameValueLocale + " WHERE index_id = :"_ns + + kStmtParamNameIndexId + " AND value = :"_ns + + kStmtParamNameValue + " AND object_data_key = :"_ns + + kStmtParamNameObjectDataKey)); + } + + mozStorageStatementScoper scoper(writeStmt); + QM_TRY(MOZ_TO_RESULT(writeStmt->BindInt64ByName(kStmtParamNameIndexId, + aIndexMetadata.id()))); + + Key oldKey, objectStorePosition; + QM_TRY(MOZ_TO_RESULT(oldKey.SetFromStatement(&readStmt, 0))); + QM_TRY(MOZ_TO_RESULT( + oldKey.BindToStatement(writeStmt, kStmtParamNameValue))); + + QM_TRY_INSPECT(const auto& newSortKey, + oldKey.ToLocaleAwareKey(aLocale)); + + QM_TRY(MOZ_TO_RESULT( + newSortKey.BindToStatement(writeStmt, kStmtParamNameValueLocale))); + QM_TRY( + MOZ_TO_RESULT(objectStorePosition.SetFromStatement(&readStmt, 1))); + QM_TRY(MOZ_TO_RESULT(objectStorePosition.BindToStatement( + writeStmt, kStmtParamNameObjectDataKey))); + + QM_TRY(MOZ_TO_RESULT(writeStmt->Execute())); + + return Ok{}; + })); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + static constexpr auto metaQuery = + "UPDATE object_store_index SET " + "locale = :locale WHERE id = :id"_ns; + + QM_TRY_INSPECT(const auto& metaStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, metaQuery)); + + QM_TRY(MOZ_TO_RESULT( + metaStmt->BindStringByIndex(0, NS_ConvertASCIItoUTF16(aLocale)))); + + QM_TRY(MOZ_TO_RESULT(metaStmt->BindInt64ByIndex(1, aIndexMetadata.id()))); + + QM_TRY(MOZ_TO_RESULT(metaStmt->Execute())); + + return NS_OK; +} + +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(); + QM_TRY(MOZ_TO_RESULT(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + EnsureDatabaseActor(); + + if (mDatabase->IsInvalidated()) { + IDB_REPORT_INTERNAL_ERR(); + QM_TRY(MOZ_TO_RESULT(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.unsafeGetRawPtr())); + MOZ_ASSERT(!info->mWaitingFactoryOp); + MOZ_ASSERT(info->mMetadata == mMetadata); + + auto transaction = MakeSafeRefPtr<VersionChangeTransaction>(this); + + if (NS_WARN_IF(!transaction->CopyDatabaseMetadata())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + MOZ_ASSERT(info->mMetadata != mMetadata); + mMetadata = info->mMetadata.clonePtr(); + + const Maybe<uint64_t> newVersion = Some(mRequestedVersion); + + QM_TRY(MOZ_TO_RESULT(SendVersionChangeMessages( + info, mDatabase.maybeDeref(), mMetadata->mCommonMetadata.version(), + newVersion))); + + mVersionChangeTransaction = std::move(transaction); + + if (mMaybeBlockedDatabases.IsEmpty()) { + // We don't need to wait on any databases, just jump to the transaction + // pool. + WaitForTransactions(); + return NS_OK; + } + + // If the actor gets destroyed, mWaitingFactoryOp will hold the last strong + // reference to us. + info->mWaitingFactoryOp = this; + + mState = State::WaitingForOtherDatabasesToClose; + return NS_OK; +} + +bool OpenDatabaseOp::AreActorsAlive() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mDatabase); + + return !(IsActorDestroyed() || mDatabase->IsActorDestroyed()); +} + +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::Mode::VersionChange); + 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->Init(transactionId); + + return NS_OK; +} + +nsresult OpenDatabaseOp::SendUpgradeNeeded() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseWorkVersionChange); + MOZ_ASSERT(mVersionChangeTransaction); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT(!HasFailed()); + MOZ_ASSERT_IF(!IsActorDestroyed(), mDatabase); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const SafeRefPtr<VersionChangeTransaction> transaction = + std::move(mVersionChangeTransaction); + + nsresult rv = EnsureDatabaseActorIsAlive(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Transfer ownership to IPDL. + transaction->SetActorAlive(); + + if (!mDatabase->SendPBackgroundIDBVersionChangeTransactionConstructor( + transaction.unsafeGetRawPtr(), 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(mMaybeBlockedDatabases.IsEmpty()); + MOZ_ASSERT_IF(!HasFailed(), !mVersionChangeTransaction); + + DebugOnly<DatabaseActorInfo*> info = nullptr; + MOZ_ASSERT_IF( + gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info), + !info->mWaitingFactoryOp); + + if (mVersionChangeTransaction) { + MOZ_ASSERT(HasFailed()); + + mVersionChangeTransaction->Abort(ResultCode(), /* aForce */ true); + mVersionChangeTransaction = nullptr; + } + + if (IsActorDestroyed()) { + SetFailureCodeIfUnset(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } else { + FactoryRequestResponse response; + + if (!HasFailed()) { + // 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. + + // XXX OpenDatabaseRequestResponse stores a raw pointer, can this be + // avoided? + response = OpenDatabaseRequestResponse{ + WrapNotNull(mDatabase.unsafeGetRawPtr())}; + } else { + response = ClampResultCode(rv); +#ifdef DEBUG + SetFailureCode(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(ResultCode()); + } + + Unused << PBackgroundIDBFactoryRequestParent::Send__delete__(this, + response); + } + + if (mDatabase) { + MOZ_ASSERT(!mDirectoryLock); + + if (HasFailed()) { + mDatabase->Invalidate(); + } + + // Make sure to release the database on this thread. + mDatabase = nullptr; + + CleanupMetadata(); + } else if (mDirectoryLock) { + // ConnectionClosedCallback will call CleanupMetadata(). + nsCOMPtr<nsIRunnable> callback = NewRunnableMethod( + "dom::indexedDB::OpenDatabaseOp::ConnectionClosedCallback", this, + &OpenDatabaseOp::ConnectionClosedCallback); + + RefPtr<WaitForTransactionsHelper> helper = + new WaitForTransactionsHelper(mDatabaseId, callback); + helper->WaitForTransactions(); + } else { + CleanupMetadata(); + } + + FinishSendResults(); +} + +void OpenDatabaseOp::ConnectionClosedCallback() { + AssertIsOnOwningThread(); + MOZ_ASSERT(HasFailed()); + MOZ_ASSERT(mDirectoryLock); + + mDirectoryLock = nullptr; + + CleanupMetadata(); +} + +void OpenDatabaseOp::EnsureDatabaseActor() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::BeginVersionChange || + mState == State::DatabaseWorkVersionChange || + mState == State::SendingResults); + MOZ_ASSERT(!HasFailed()); + 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.clonePtr(); + } + + Maybe<const CipherKey> maybeKey = + mInPrivateBrowsing ? mFileManager->MutableCipherKeyManagerRef().Get() + : Nothing(); + + MOZ_RELEASE_ASSERT(mInPrivateBrowsing == maybeKey.isSome()); + + // XXX Shouldn't Manager() return already_AddRefed when + // PBackgroundIDBFactoryParent is declared refcounted? + mDatabase = MakeSafeRefPtr<Database>( + SafeRefPtr{static_cast<Factory*>(Manager()), + AcquireStrongRefFromRawPtr{}}, + mCommonParams.principalInfo(), + mContentHandle ? Some(mContentHandle->ChildID()) : Nothing(), + mOriginMetadata, mTelemetryId, mMetadata.clonePtr(), + mFileManager.clonePtr(), std::move(mDirectoryLock), + mChromeWriteAccessAllowed, mInPrivateBrowsing, maybeKey); + + if (info) { + info->mLiveDatabases.AppendElement( + WrapNotNullUnchecked(mDatabase.unsafeGetRawPtr())); + } else { + // XXX Maybe use LookupOrInsertWith above, to avoid a second lookup here? + info = gLiveDatabaseHashtable + ->InsertOrUpdate( + mDatabaseId, + MakeUnique<DatabaseActorInfo>( + mMetadata.clonePtr(), + WrapNotNullUnchecked(mDatabase.unsafeGetRawPtr()))) + .get(); + } + + // Balanced in Database::CleanupMetadata(). + IncreaseBusyCount(); +} + +nsresult OpenDatabaseOp::EnsureDatabaseActorIsAlive() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseWorkVersionChange || + mState == State::SendingResults); + MOZ_ASSERT(!HasFailed()); + MOZ_ASSERT(!IsActorDestroyed()); + + EnsureDatabaseActor(); + + if (mDatabase->IsActorAlive()) { + return NS_OK; + } + + auto* const factory = static_cast<Factory*>(Manager()); + + QM_TRY_INSPECT(const auto& spec, MetadataToSpec()); + + // Transfer ownership to IPDL. + mDatabase->SetActorAlive(); + + if (!factory->SendPBackgroundIDBDatabaseConstructor( + mDatabase.unsafeGetRawPtr(), spec, WrapNotNull(this))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + return NS_OK; +} + +Result<DatabaseSpec, nsresult> OpenDatabaseOp::MetadataToSpec() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + DatabaseSpec spec; + spec.metadata() = mMetadata->mCommonMetadata; + + QM_TRY_UNWRAP(spec.objectStores(), + TransformIntoNewArrayAbortOnErr( + mMetadata->mObjectStores, + [](const auto& objectStoreEntry) + -> mozilla::Result<ObjectStoreSpec, nsresult> { + FullObjectStoreMetadata* metadata = + objectStoreEntry.GetWeak(); + MOZ_ASSERT(objectStoreEntry.GetKey()); + MOZ_ASSERT(metadata); + + ObjectStoreSpec objectStoreSpec; + objectStoreSpec.metadata() = metadata->mCommonMetadata; + + QM_TRY_UNWRAP(auto indexes, + TransformIntoNewArray( + metadata->mIndexes, + [](const auto& indexEntry) { + FullIndexMetadata* indexMetadata = + indexEntry.GetWeak(); + MOZ_ASSERT(indexEntry.GetKey()); + MOZ_ASSERT(indexMetadata); + + return indexMetadata->mCommonMetadata; + }, + fallible)); + + objectStoreSpec.indexes() = std::move(indexes); + + return objectStoreSpec; + }, + fallible)); + + return spec; +} + +#ifdef DEBUG + +void OpenDatabaseOp::AssertMetadataConsistency( + const FullDatabaseMetadata& aMetadata) { + AssertIsOnBackgroundThread(); + + const FullDatabaseMetadata& thisDB = *mMetadata; + const FullDatabaseMetadata& otherDB = aMetadata; + + 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 (const auto& thisObjectStore : thisDB.mObjectStores.Values()) { + MOZ_ASSERT(thisObjectStore); + MOZ_ASSERT(!thisObjectStore->mDeleted); + + auto otherObjectStore = MatchMetadataNameOrId( + otherDB.mObjectStores, thisObjectStore->mCommonMetadata.id()); + MOZ_ASSERT(otherObjectStore); + + MOZ_ASSERT(thisObjectStore != &otherObjectStore.ref()); + + 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. + // TODO The first part of the comment should probably be rephrased. I think + // it still applies but it sounds as if this were thread-unsafe like it was + // before, which isn't true anymore. + { + const auto&& thisAutoIncrementIds = + thisObjectStore->mAutoIncrementIds.Lock(); + const auto&& otherAutoIncrementIds = + otherObjectStore->mAutoIncrementIds.Lock(); + + MOZ_ASSERT(thisAutoIncrementIds->next <= otherAutoIncrementIds->next); + MOZ_ASSERT( + thisAutoIncrementIds->committed <= otherAutoIncrementIds->committed || + thisAutoIncrementIds->committed == otherAutoIncrementIds->next); + } + MOZ_ASSERT(!otherObjectStore->mDeleted); + + MOZ_ASSERT(thisObjectStore->mIndexes.Count() == + otherObjectStore->mIndexes.Count()); + + for (const auto& thisIndex : thisObjectStore->mIndexes.Values()) { + MOZ_ASSERT(thisIndex); + MOZ_ASSERT(!thisIndex->mDeleted); + + auto otherIndex = MatchMetadataNameOrId(otherObjectStore->mIndexes, + thisIndex->mCommonMetadata.id()); + MOZ_ASSERT(otherIndex); + + MOZ_ASSERT(thisIndex != &otherIndex.ref()); + + 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; + } + + AUTO_PROFILER_LABEL("OpenDatabaseOp::VersionChangeOp::DoDatabaseWork", DOM); + + IDB_LOG_MARK_PARENT_TRANSACTION("Beginning database work", "DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransactionLoggingSerialNumber); + + Transaction().SetActiveOnConnectionThread(); + + QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction())); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "UPDATE database SET version = :version;"_ns, + ([&self = *this]( + mozIStorageStatement& updateStmt) -> mozilla::Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + updateStmt.BindInt64ByIndex(0, int64_t(self.mRequestedVersion)))); + + return Ok{}; + })))); + + 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. + // + // XXX: Depending on timing, in most cases, NoteActorDestroyed will not have + // been destroyed before, but in some cases it has. This should be reworked in + // a way this hack is not necessary. There are also several similar cases in + // other *Op classes. + if (!IsActorDestroyed()) { + NoteActorDestroyed(); + } +#endif + + TransactionDatabaseOperationBase::Cleanup(); +} + +void DeleteDatabaseOp::LoadPreviousVersion(nsIFile& aDatabaseFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + MOZ_ASSERT(!mPreviousVersion); + + AUTO_PROFILER_LABEL("DeleteDatabaseOp::LoadPreviousVersion", DOM); + + nsresult rv; + + nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + IndexedDatabaseManager* const idm = IndexedDatabaseManager::Get(); + MOZ_ASSERT(idm); + + const PersistenceType persistenceType = + mCommonParams.metadata().persistenceType(); + const nsAString& databaseName = mCommonParams.metadata().name(); + + SafeRefPtr<DatabaseFileManager> fileManager = idm->GetFileManager( + persistenceType, mOriginMetadata.mOrigin, databaseName); + + if (!fileManager) { + fileManager = MakeSafeRefPtr<DatabaseFileManager>( + persistenceType, mOriginMetadata, databaseName, mDatabaseId, + mEnforcingQuota, mInPrivateBrowsing); + } + + const auto maybeKey = + mInPrivateBrowsing + ? Some(fileManager->MutableCipherKeyManagerRef().Ensure()) + : Nothing(); + + MOZ_RELEASE_ASSERT(mInPrivateBrowsing == maybeKey.isSome()); + + // Pass -1 as the directoryLockId to disable quota checking, since we might + // temporarily exceed quota before deleting the database. + QM_TRY_INSPECT(const auto& dbFileUrl, + GetDatabaseFileURL(aDatabaseFile, -1, maybeKey), QM_VOID); + + QM_TRY_UNWRAP(const NotNull<nsCOMPtr<mozIStorageConnection>> connection, + OpenDatabaseAndHandleBusy(*ss, *dbFileUrl), QM_VOID); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *connection, "SELECT name FROM database"_ns), + QM_VOID); + + QM_TRY(OkIf(stmt), QM_VOID); + + nsString databaseName; + rv = stmt->GetString(0, databaseName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT(mCommonParams.metadata().name() == databaseName); + } +#endif + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *connection, "SELECT version FROM database"_ns), + QM_VOID); + + QM_TRY(OkIf(stmt), QM_VOID); + + 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); + + nsresult rv = SendToIOThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult DeleteDatabaseOp::DoDatabaseWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + MOZ_ASSERT(mOriginMetadata.mPersistenceType == + mCommonParams.metadata().persistenceType()); + + AUTO_PROFILER_LABEL("DeleteDatabaseOp::DoDatabaseWork", DOM); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + const nsAString& databaseName = mCommonParams.metadata().name(); + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(auto directory, + quotaManager->GetOriginDirectory(mOriginMetadata)); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); + + QM_TRY_UNWRAP(mDatabaseDirectoryPath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, directory, GetPath)); + + mDatabaseFilenameBase = + GetDatabaseFilenameBase(databaseName, mOriginMetadata.mIsPrivate); + + QM_TRY_INSPECT( + const auto& dbFile, + CloneFileAndAppend(*directory, mDatabaseFilenameBase + kSQLiteSuffix)); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const auto& databaseFilePath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); + + MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); + } +#endif + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(dbFile, Exists)); + + 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; + } + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + 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(); + QM_TRY(MOZ_TO_RESULT(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + DatabaseActorInfo* info; + if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { + MOZ_ASSERT(!info->mWaitingFactoryOp); + + nsresult rv = + SendVersionChangeMessages(info, Nothing(), mPreviousVersion, Nothing()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!mMaybeBlockedDatabases.IsEmpty()) { + // If the actor gets destroyed, mWaitingFactoryOp will hold the last + // strong reference to us. + 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; +} + +bool DeleteDatabaseOp::AreActorsAlive() { + AssertIsOnOwningThread(); + + return !IsActorDestroyed(); +} + +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* const 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::SendBlockedNotification() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::WaitingForOtherDatabasesToClose); + + if (!IsActorDestroyed()) { + Unused << SendBlocked(mPreviousVersion); + } +} + +void DeleteDatabaseOp::SendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); + + DebugOnly<DatabaseActorInfo*> info = nullptr; + MOZ_ASSERT_IF( + gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info), + !info->mWaitingFactoryOp); + + if (!IsActorDestroyed()) { + FactoryRequestResponse response; + + if (!HasFailed()) { + response = DeleteDatabaseRequestResponse(mPreviousVersion); + } else { + response = ClampResultCode(ResultCode()); + } + + Unused << PBackgroundIDBFactoryRequestParent::Send__delete__(this, + response); + } + + mDirectoryLock = nullptr; + + CleanupMetadata(); + + FinishSendResults(); +} + +nsresult DeleteDatabaseOp::VersionChangeOp::RunOnIOThread() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDeleteDatabaseOp->mState == State::DatabaseWorkVersionChange); + + AUTO_PROFILER_LABEL("DeleteDatabaseOp::VersionChangeOp::RunOnIOThread", DOM); + + 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; + } + + nsresult rv = RemoveDatabaseFilesAndDirectory( + *directory, mDeleteDatabaseOp->mDatabaseFilenameBase, quotaManager, + persistenceType, mDeleteDatabaseOp->mOriginMetadata, + mDeleteDatabaseOp->mCommonParams.metadata().name()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mOwningEventTarget->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); + + const RefPtr<DeleteDatabaseOp> deleteOp = std::move(mDeleteDatabaseOp); + + if (deleteOp->IsActorDestroyed()) { + IDB_REPORT_INTERNAL_ERR(); + deleteOp->SetFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } else if (HasFailed()) { + deleteOp->SetFailureCodeIfUnset(ResultCode()); + } else { + DatabaseActorInfo* info; + + // Inform all the other databases that they are now invalidated. That + // should remove the previous metadata from our table. + if (gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId, &info)) { + MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); + MOZ_ASSERT(!info->mWaitingFactoryOp); + + nsTArray<SafeRefPtr<Database>> liveDatabases; + if (NS_WARN_IF(!liveDatabases.SetCapacity(info->mLiveDatabases.Length(), + fallible))) { + deleteOp->SetFailureCode(NS_ERROR_OUT_OF_MEMORY); + } else { + std::transform(info->mLiveDatabases.cbegin(), + info->mLiveDatabases.cend(), + MakeBackInserter(liveDatabases), + [](const auto& aDatabase) -> SafeRefPtr<Database> { + return {aDatabase.get(), AcquireStrongRefFromRawPtr{}}; + }); + +#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 (const auto& database : liveDatabases) { + 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))) { + SetFailureCodeIfUnset(rv); + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + return NS_OK; +} + +TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( + SafeRefPtr<TransactionBase> aTransaction) + : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), + aTransaction->GetLoggingInfo()->NextRequestSN()), + mTransaction(WrapNotNull(std::move(aTransaction))), + mTransactionIsAborted((*mTransaction)->IsAborted()), + mTransactionLoggingSerialNumber((*mTransaction)->LoggingSerialNumber()) { + MOZ_ASSERT(LoggingSerialNumber()); +} + +TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( + SafeRefPtr<TransactionBase> aTransaction, uint64_t aLoggingSerialNumber) + : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), + aLoggingSerialNumber), + mTransaction(WrapNotNull(std::move(aTransaction))), + mTransactionIsAborted((*mTransaction)->IsAborted()), + mTransactionLoggingSerialNumber((*mTransaction)->LoggingSerialNumber()) {} + +TransactionDatabaseOperationBase::~TransactionDatabaseOperationBase() { + MOZ_ASSERT(mInternalState == InternalState::Completed); + MOZ_ASSERT(!mTransaction, + "TransactionDatabaseOperationBase::Cleanup() was not called by a " + "subclass!"); +} + +#ifdef DEBUG + +void TransactionDatabaseOperationBase::AssertIsOnConnectionThread() const { + (*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(!HasFailed()); + + AUTO_PROFILER_LABEL("TransactionDatabaseOperationBase::RunOnConnectionThread", + DOM); + + // 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. + SetFailureCode(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(); + OverrideFailureCode(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } else { + Database& database = (*mTransaction)->GetMutableDatabase(); + + // Here we're actually going to perform the database operation. + nsresult rv = database.EnsureConnection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + SetFailureCode(rv); + } else { + DatabaseConnection* connection = database.GetConnection(); + MOZ_ASSERT(connection); + + auto& storageConnection = connection->MutableStorageConnection(); + + AutoSetProgressHandler autoProgress; + if (mLoggingSerialNumber) { + rv = autoProgress.Register(storageConnection, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + SetFailureCode(rv); + } + } + + if (NS_SUCCEEDED(rv)) { + if (mLoggingSerialNumber) { + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "Beginning database work", "DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransactionLoggingSerialNumber, mLoggingSerialNumber); + } + + rv = DoDatabaseWork(connection); + + if (mLoggingSerialNumber) { + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "Finished database work", "DB End", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransactionLoggingSerialNumber, mLoggingSerialNumber); + } + + if (NS_FAILED(rv)) { + SetFailureCode(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(mOwningEventTarget->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); + + mWaitingForContinue = false; + + 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); + + // The flag is raised only when there is no mUpdateRefcountFunction for the + // executing operation. It assume that is because the previous + // StartTransactionOp was failed to begin a write transaction and it reported + // when this operation has already jumped to the Connection thread. + MOZ_DIAGNOSTIC_ASSERT_IF(mAssumingPreviousOperationFail, + (*mTransaction)->IsAborted()); + + if (NS_WARN_IF(IsActorDestroyed())) { + // Normally we wouldn't need to send any notifications if the actor was + // already destroyed, but this can be a VersionChangeOp which needs to + // notify its parent operation (OpenDatabaseOp) about the failure. + // So SendFailureResult needs to be called even when the actor was + // destroyed. Normal operations redundantly check if the actor was + // destroyed in SendSuccessResult and SendFailureResult, therefore it's + // ok to call it in all cases here. + if (!HasFailed()) { + IDB_REPORT_INTERNAL_ERR(); + SetFailureCode(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. + OverrideFailureCode(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + } + + const nsresult rv = [aSendPreprocessInfo, this] { + if (HasFailed()) { + return ResultCode(); + } + if (aSendPreprocessInfo) { + // This should not release the IPDL reference. + return SendPreprocessInfo(); + } + // This may release the IPDL reference. + return SendSuccessResult(); + }(); + + if (NS_FAILED(rv)) { + SetFailureCodeIfUnset(rv); + + // This should definitely release the IPDL reference. + if (!SendFailureResult(rv)) { + // Abort the transaction. + (*mTransaction)->Abort(rv, /* aForce */ false); + } + } + + if (aSendPreprocessInfo && !HasFailed()) { + mInternalState = InternalState::WaitingForContinue; + + mWaitingForContinue = true; + } else { + if (mLoggingSerialNumber) { + (*mTransaction)->NoteFinishedRequest(mLoggingSerialNumber, ResultCode()); + } + + Cleanup(); + + mInternalState = InternalState::Completed; + } +} + +bool TransactionDatabaseOperationBase::Init(TransactionBase& aTransaction) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mInternalState == InternalState::Initial); + + return true; +} + +void TransactionDatabaseOperationBase::Cleanup() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mInternalState == InternalState::SendingResults); + + mTransaction.destroy(); +} + +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(SafeRefPtr<TransactionBase> aTransaction, + nsresult aResultCode) + : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), + aTransaction->GetLoggingInfo()->NextRequestSN()), + mTransaction(std::move(aTransaction)), + mResultCode(aResultCode) { + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(LoggingSerialNumber()); +} + +nsresult TransactionBase::CommitOp::WriteAutoIncrementCounts() { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::Mode::ReadWrite || + mTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush || + mTransaction->GetMode() == IDBTransaction::Mode::Cleanup || + mTransaction->GetMode() == IDBTransaction::Mode::VersionChange); + + const nsTArray<SafeRefPtr<FullObjectStoreMetadata>>& metadataArray = + mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; + + if (!metadataArray.IsEmpty()) { + DatabaseConnection* connection = + mTransaction->GetDatabase().GetConnection(); + MOZ_ASSERT(connection); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + auto stmt = DatabaseConnection::LazyStatement( + *connection, + "UPDATE object_store " + "SET auto_increment = :auto_increment WHERE id " + "= :object_store_id;"_ns); + + for (const auto& metadata : metadataArray) { + MOZ_ASSERT(!metadata->mDeleted); + + const int64_t nextAutoIncrementId = [&metadata] { + const auto&& lockedAutoIncrementIds = + metadata->mAutoIncrementIds.Lock(); + return lockedAutoIncrementIds->next; + }(); + + MOZ_ASSERT(nextAutoIncrementId > 1); + + QM_TRY_INSPECT(const auto& borrowedStmt, stmt.Borrow()); + + QM_TRY(MOZ_TO_RESULT( + borrowedStmt->BindInt64ByIndex(1, metadata->mCommonMetadata.id()))); + + QM_TRY(MOZ_TO_RESULT( + borrowedStmt->BindInt64ByIndex(0, nextAutoIncrementId))); + + QM_TRY(MOZ_TO_RESULT(borrowedStmt->Execute())); + } + } + + return NS_OK; +} + +void TransactionBase::CommitOp::CommitOrRollbackAutoIncrementCounts() { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + MOZ_ASSERT(mTransaction->GetMode() == IDBTransaction::Mode::ReadWrite || + mTransaction->GetMode() == IDBTransaction::Mode::ReadWriteFlush || + mTransaction->GetMode() == IDBTransaction::Mode::Cleanup || + mTransaction->GetMode() == IDBTransaction::Mode::VersionChange); + + const auto& metadataArray = + mTransaction->mModifiedAutoIncrementObjectStoreMetadataArray; + + if (!metadataArray.IsEmpty()) { + bool committed = NS_SUCCEEDED(mResultCode); + + for (const auto& metadata : metadataArray) { + auto&& lockedAutoIncrementIds = metadata->mAutoIncrementIds.Lock(); + + if (committed) { + lockedAutoIncrementIds->committed = lockedAutoIncrementIds->next; + } else { + lockedAutoIncrementIds->next = lockedAutoIncrementIds->committed; + } + } + } +} + +#ifdef DEBUG + +void TransactionBase::CommitOp::AssertForeignKeyConsistency( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + MOZ_ASSERT(mTransaction->GetMode() != IDBTransaction::Mode::ReadOnly); + + { + QM_TRY_INSPECT( + const auto& pragmaStmt, + CreateAndExecuteSingleStepStatement( + aConnection->MutableStorageConnection(), "PRAGMA foreign_keys;"_ns), + QM_ASSERT_UNREACHABLE_VOID); + + int32_t foreignKeysEnabled; + MOZ_ALWAYS_SUCCEEDS(pragmaStmt->GetInt32(0, &foreignKeysEnabled)); + + MOZ_ASSERT(foreignKeysEnabled, + "Database doesn't have foreign keys enabled!"); + } + + { + QM_TRY_INSPECT(const bool& foreignKeyError, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection->MutableStorageConnection(), + "PRAGMA foreign_key_check;"_ns), + QM_ASSERT_UNREACHABLE_VOID); + + MOZ_ASSERT(!foreignKeyError, "Database has inconsisistent foreign keys!"); + } +} + +#endif // DEBUG + +NS_IMPL_ISUPPORTS_INHERITED0(TransactionBase::CommitOp, DatabaseOperationBase) + +NS_IMETHODIMP +TransactionBase::CommitOp::Run() { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("TransactionBase::CommitOp::Run", DOM); + + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "Beginning database work", "DB Start", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransaction->LoggingSerialNumber(), mLoggingSerialNumber); + + if (mTransaction->GetMode() != IDBTransaction::Mode::ReadOnly && + mTransaction->mHasBeenActiveOnConnectionThread) { + if (DatabaseConnection* connection = + mTransaction->GetDatabase().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::Mode::ReadWriteFlush) { + 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::Mode::Cleanup) { + connection->DoIdleProcessing(/* aNeedsCheckpoint */ true); + + connection->EnableQuotaChecks(); + } + } + } + + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "Finished database work", "DB End", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransaction->LoggingSerialNumber(), mLoggingSerialNumber); + + IDB_LOG_MARK_PARENT_TRANSACTION("Finished database work", "DB End", + IDB_LOG_ID_STRING(mBackgroundChildLoggingId), + mTransaction->LoggingSerialNumber()); + + return NS_OK; +} + +void TransactionBase::CommitOp::TransactionFinishedBeforeUnblock() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction); + + AUTO_PROFILER_LABEL("CommitOp::TransactionFinishedBeforeUnblock", DOM); + + if (!IsActorDestroyed()) { + mTransaction->UpdateMetadata(mResultCode); + } +} + +void TransactionBase::CommitOp::TransactionFinishedAfterUnblock() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction); + + IDB_LOG_MARK_PARENT_TRANSACTION( + "Finished with result 0x%" PRIx32, "Transaction finished (0x%" PRIx32 ")", + IDB_LOG_ID_STRING(mTransaction->GetLoggingInfo()->Id()), + mTransaction->LoggingSerialNumber(), static_cast<uint32_t>(mResultCode)); + + mTransaction->SendCompleteNotification(ClampResultCode(mResultCode)); + + mTransaction->GetMutableDatabase().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 +} + +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(); + + AUTO_PROFILER_LABEL("CreateObjectStoreOp::DoDatabaseWork", DOM); + +#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... + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT(const bool& hasResult, + aConnection + ->BorrowAndExecuteSingleStepStatement( + "SELECT name " + "FROM object_store " + "WHERE name = :name;"_ns, + [&self = *this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex( + 0, self.mMetadata.name()))); + return Ok{}; + }) + .map(IsSome), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(!hasResult); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "INSERT INTO object_store (id, auto_increment, name, key_path) " + "VALUES (:id, :auto_increment, :name, :key_path);"_ns, + [&metadata = + mMetadata](mozIStorageStatement& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, metadata.id()))); + + QM_TRY(MOZ_TO_RESULT( + stmt.BindInt32ByIndex(1, metadata.autoIncrement() ? 1 : 0))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(2, metadata.name()))); + + if (metadata.keyPath().IsValid()) { + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex( + 3, metadata.keyPath().SerializeToString()))); + } else { + QM_TRY(MOZ_TO_RESULT(stmt.BindNullByIndex(3))); + } + + return Ok{}; + }))); + +#ifdef DEBUG + { + int64_t id; + MOZ_ALWAYS_SUCCEEDS( + aConnection->MutableStorageConnection().GetLastInsertRowID(&id)); + MOZ_ASSERT(mMetadata.id() == id); + } +#endif + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +nsresult DeleteObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("DeleteObjectStoreOp::DoDatabaseWork", DOM); + +#ifdef DEBUG + { + // Make sure |mIsLastObjectStore| is telling the truth. + QM_TRY_INSPECT( + const auto& stmt, + aConnection->BorrowCachedStatement("SELECT id FROM object_store;"_ns), + QM_ASSERT_UNREACHABLE); + + 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; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + if (mIsLastObjectStore) { + // We can just delete everything if this is the last object store. + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteCachedStatement("DELETE FROM index_data;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM unique_index_data;"_ns))); + + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteCachedStatement("DELETE FROM object_data;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM object_store_index;"_ns))); + + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteCachedStatement("DELETE FROM object_store;"_ns))); + } else { + QM_TRY_INSPECT( + const bool& hasIndexes, + ObjectStoreHasIndexes(*aConnection, mMetadata->mCommonMetadata.id())); + + const auto bindObjectStoreIdToFirstParameter = + [this](mozIStorageStatement& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + stmt.BindInt64ByIndex(0, mMetadata->mCommonMetadata.id()))); + + return Ok{}; + }; + + // The parameter name :object_store_id in the SQL statements below is not + // used for binding, parameters are bound by index only locally by + // bindObjectStoreIdToFirstParameter. + if (hasIndexes) { + QM_TRY(MOZ_TO_RESULT(DeleteObjectStoreDataTableRowsWithIndexes( + aConnection, mMetadata->mCommonMetadata.id(), Nothing()))); + + // Now clean up the object store index table. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM object_store_index " + "WHERE object_store_id = :object_store_id;"_ns, + bindObjectStoreIdToFirstParameter))); + } else { + // We only have to worry about object data if this object store has no + // indexes. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM object_data " + "WHERE object_store_id = :object_store_id;"_ns, + bindObjectStoreIdToFirstParameter))); + } + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM object_store " + "WHERE id = :object_store_id;"_ns, + bindObjectStoreIdToFirstParameter))); + +#ifdef DEBUG + { + int32_t deletedRowCount; + MOZ_ALWAYS_SUCCEEDS( + aConnection->MutableStorageConnection().GetAffectedRows( + &deletedRowCount)); + MOZ_ASSERT(deletedRowCount == 1); + } +#endif + } + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + if (mMetadata->mCommonMetadata.autoIncrement()) { + Transaction().ForgetModifiedAutoIncrementObjectStore(*mMetadata); + } + + return NS_OK; +} + +nsresult RenameObjectStoreOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("RenameObjectStoreOp::DoDatabaseWork", DOM); + +#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... + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT( + const bool& hasResult, + aConnection + ->BorrowAndExecuteSingleStepStatement( + "SELECT name " + "FROM object_store " + "WHERE name = :name AND id != :id;"_ns, + [&self = *this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY( + MOZ_TO_RESULT(stmt.BindStringByIndex(0, self.mNewName))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(1, self.mId))); + return Ok{}; + }) + .map(IsSome), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(!hasResult); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "UPDATE object_store " + "SET name = :name " + "WHERE id = :id;"_ns, + [&self = *this](mozIStorageStatement& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(0, self.mNewName))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(1, self.mId))); + + return Ok{}; + }))); + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +CreateIndexOp::CreateIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + const IndexOrObjectStoreId aObjectStoreId, + const IndexMetadata& aMetadata) + : VersionChangeTransactionOp(std::move(aTransaction)), + mMetadata(aMetadata), + mFileManager(Transaction().GetDatabase().GetFileManagerPtr()), + mDatabaseId(Transaction().DatabaseId()), + mObjectStoreId(aObjectStoreId) { + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(aMetadata.id()); + MOZ_ASSERT(mFileManager); + MOZ_ASSERT(!mDatabaseId.IsEmpty()); +} + +nsresult CreateIndexOp::InsertDataFromObjectStore( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mMaybeUniqueIndexTable); + + AUTO_PROFILER_LABEL("CreateIndexOp::InsertDataFromObjectStore", DOM); + + auto& storageConnection = aConnection->MutableStorageConnection(); + + RefPtr<UpdateIndexDataValuesFunction> updateFunction = + new UpdateIndexDataValuesFunction(this, aConnection, + Transaction().GetDatabasePtr()); + + constexpr auto updateFunctionName = "update_index_data_values"_ns; + + 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) const { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mMaybeUniqueIndexTable); + + MOZ_ASSERT(aConnection->HasStorageConnection()); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "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;"_ns, + [objectStoredId = + mObjectStoreId](mozIStorageStatement& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, objectStoredId))); + + return Ok{}; + }))); + + return NS_OK; +} + +bool CreateIndexOp::Init(TransactionBase& aTransaction) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mObjectStoreId); + MOZ_ASSERT(mMaybeUniqueIndexTable.isNothing()); + + const SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + aTransaction.GetMetadataForObjectStoreId(mObjectStoreId); + MOZ_ASSERT(objectStoreMetadata); + + const uint32_t indexCount = objectStoreMetadata->mIndexes.Count(); + if (!indexCount) { + return true; + } + + auto uniqueIndexTable = UniqueIndexTable{indexCount}; + + for (const auto& value : objectStoreMetadata->mIndexes.Values()) { + MOZ_ASSERT(!uniqueIndexTable.Contains(value->mCommonMetadata.id())); + + if (NS_WARN_IF(!uniqueIndexTable.InsertOrUpdate( + value->mCommonMetadata.id(), value->mCommonMetadata.unique(), + fallible))) { + IDB_REPORT_INTERNAL_ERR(); + NS_WARNING("out of memory"); + return false; + } + } + + uniqueIndexTable.MarkImmutable(); + + mMaybeUniqueIndexTable.emplace(std::move(uniqueIndexTable)); + + return true; +} + +nsresult CreateIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("CreateIndexOp::DoDatabaseWork", DOM); + +#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... + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT( + const bool& hasResult, + aConnection + ->BorrowAndExecuteSingleStepStatement( + "SELECT name " + "FROM object_store_index " + "WHERE object_store_id = :object_store_id AND name = :name;"_ns, + [&self = *this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + stmt.BindInt64ByIndex(0, self.mObjectStoreId))); + QM_TRY(MOZ_TO_RESULT( + stmt.BindStringByIndex(1, self.mMetadata.name()))); + return Ok{}; + }) + .map(IsSome), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(!hasResult); + } +#endif + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "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, " + ":object_store_id, :locale, :is_auto_locale)"_ns, + [&metadata = mMetadata, objectStoreId = mObjectStoreId]( + mozIStorageStatement& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, metadata.id()))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(1, metadata.name()))); + + QM_TRY(MOZ_TO_RESULT( + stmt.BindStringByIndex(2, metadata.keyPath().SerializeToString()))); + + QM_TRY( + MOZ_TO_RESULT(stmt.BindInt32ByIndex(3, metadata.unique() ? 1 : 0))); + + QM_TRY(MOZ_TO_RESULT( + stmt.BindInt32ByIndex(4, metadata.multiEntry() ? 1 : 0))); + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(5, objectStoreId))); + + QM_TRY(MOZ_TO_RESULT( + metadata.locale().IsEmpty() + ? stmt.BindNullByIndex(6) + : stmt.BindUTF8StringByIndex(6, metadata.locale()))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByIndex(7, metadata.autoLocale()))); + + return Ok{}; + }))); + +#ifdef DEBUG + { + int64_t id; + MOZ_ALWAYS_SUCCEEDS( + aConnection->MutableStorageConnection().GetLastInsertRowID(&id)); + MOZ_ASSERT(mMetadata.id() == id); + } +#endif + + QM_TRY(MOZ_TO_RESULT(InsertDataFromObjectStore(aConnection))); + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +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(mOp->mFileManager); + + AUTO_PROFILER_LABEL( + "CreateIndexOp::UpdateIndexDataValuesFunction::OnFunctionCall", DOM); + +#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 + + QM_TRY_UNWRAP(auto cloneInfo, GetStructuredCloneReadInfoFromValueArray( + aValues, + /* aDataIndex */ 3, + /* aFileIdsIndex */ 2, *mOp->mFileManager)); + + const IndexMetadata& metadata = mOp->mMetadata; + const IndexOrObjectStoreId& objectStoreId = mOp->mObjectStoreId; + + // XXX does this really need a non-const cloneInfo? + QM_TRY_INSPECT(const auto& updateInfos, + DeserializeIndexValueToUpdateInfos( + metadata.id(), metadata.keyPath(), metadata.multiEntry(), + metadata.locale(), cloneInfo)); + + if (updateInfos.IsEmpty()) { + // XXX See if we can do this without copying... + + nsCOMPtr<nsIVariant> unmodifiedValue; + + // No changes needed, just return the original value. + QM_TRY_INSPECT(const int32_t& valueType, + MOZ_TO_RESULT_INVOKE_MEMBER(aValues, GetTypeOfIndex, 1)); + + 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; + QM_TRY( + MOZ_TO_RESULT(aValues->GetSharedBlob(1, &blobDataLength, &blobData))); + + const 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; + QM_TRY(MOZ_TO_RESULT(key.SetFromValueArray(aValues, 0))); + + QM_TRY_UNWRAP(auto indexValues, ReadCompressedIndexDataValues(*aValues, 1)); + + const bool hadPreviousIndexValues = !indexValues.IsEmpty(); + + const uint32_t updateInfoCount = updateInfos.Length(); + + QM_TRY(OkIf(indexValues.SetCapacity(indexValues.Length() + updateInfoCount, + fallible)), + NS_ERROR_OUT_OF_MEMORY, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + // First construct the full list to update the index_data_values row. + for (const IndexUpdateInfo& info : updateInfos) { + MOZ_ALWAYS_TRUE(indexValues.InsertElementSorted( + IndexDataValue(metadata.id(), metadata.unique(), info.value(), + info.localizedValue()), + fallible)); + } + + QM_TRY_UNWRAP((auto [indexValuesBlob, indexValuesBlobLength]), + MakeCompressedIndexDataValues(indexValues)); + + 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 (const IndexUpdateInfo& info : updateInfos) { + MOZ_ALWAYS_TRUE(indexValues.InsertElementSorted( + IndexDataValue(metadata.id(), metadata.unique(), info.value(), + info.localizedValue()), + fallible)); + } + } + + QM_TRY(MOZ_TO_RESULT( + InsertIndexTableRows(mConnection, objectStoreId, key, indexValues))); + + value = new storage::AdoptedBlobVariant( + std::pair(indexValuesBlob.release(), indexValuesBlobLength)); + + value.forget(_retval); + return NS_OK; +} + +DeleteIndexOp::DeleteIndexOp(SafeRefPtr<VersionChangeTransaction> aTransaction, + const IndexOrObjectStoreId aObjectStoreId, + const IndexOrObjectStoreId aIndexId, + const bool aUnique, const bool aIsLastIndex) + : VersionChangeTransactionOp(std::move(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) const { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!aObjectStoreKey.IsUnset()); + MOZ_ASSERT_IF(!mIsLastIndex, !aIndexValues.IsEmpty()); + + AUTO_PROFILER_LABEL("DeleteIndexOp::RemoveReferencesToIndex", DOM); + + 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. + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement( + "UPDATE object_data " + "SET index_data_values = NULL " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + " AND key = :"_ns + + kStmtParamNameKey + ";"_ns)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameObjectStoreId, mObjectStoreId))); + + QM_TRY(MOZ_TO_RESULT( + aObjectStoreKey.BindToStatement(&*stmt, kStmtParamNameKey))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; + } + + { + IndexDataValue search; + search.mIndexId = mIndexId; + + // Use raw pointers for search to avoid redundant index validity checks. + // Maybe this should better be encapsulated in nsTArray. + const auto* const begin = aIndexValues.Elements(); + const auto* const end = aIndexValues.Elements() + aIndexValues.Length(); + + const auto indexIdComparator = [](const IndexDataValue& aA, + const IndexDataValue& aB) { + return aA.mIndexId < aB.mIndexId; + }; + + MOZ_ASSERT(std::is_sorted(begin, end, indexIdComparator)); + + const auto [beginRange, endRange] = + std::equal_range(begin, end, search, indexIdComparator); + if (beginRange == end) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_FILE_CORRUPTED; + } + + aIndexValues.RemoveElementsAt(beginRange - begin, endRange - beginRange); + } + + QM_TRY(MOZ_TO_RESULT(UpdateIndexValues(aConnection, mObjectStoreId, + aObjectStoreKey, aIndexValues))); + + return NS_OK; +} + +nsresult DeleteIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + +#ifdef DEBUG + { + // Make sure |mIsLastIndex| is telling the truth. + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement( + "SELECT id " + "FROM object_store_index " + "WHERE object_store_id = :object_store_id;"_ns), + QM_ASSERT_UNREACHABLE); + + MOZ_ALWAYS_SUCCEEDS(stmt->BindInt64ByIndex(0, 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 + + AUTO_PROFILER_LABEL("DeleteIndexOp::DoDatabaseWork", DOM); + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + // 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. + QM_TRY_INSPECT( + const auto& selectStmt, + aConnection->BorrowCachedStatement( + mUnique + ? (mIsLastIndex + ? "/* do not warn (bug someone else) */ " + "SELECT value, object_data_key " + "FROM unique_index_data " + "WHERE index_id = :"_ns + + kStmtParamNameIndexId + + " ORDER BY object_data_key ASC;"_ns + : "/* 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 = :"_ns + + kStmtParamNameIndexId + + " AND object_data.object_store_id = :"_ns + + kStmtParamNameObjectStoreId + + " ORDER BY unique_index_data.object_data_key ASC;"_ns) + : (mIsLastIndex + ? "/* do not warn (bug me not) */ " + "SELECT value, object_data_key " + "FROM index_data " + "WHERE index_id = :"_ns + + kStmtParamNameIndexId + + " AND object_store_id = :"_ns + + kStmtParamNameObjectStoreId + + " ORDER BY object_data_key ASC;"_ns + : "/* 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 = :"_ns + + kStmtParamNameIndexId + + " AND object_data.object_store_id = :"_ns + + kStmtParamNameObjectStoreId + + " ORDER BY index_data.object_data_key ASC;"_ns))); + + QM_TRY(MOZ_TO_RESULT( + selectStmt->BindInt64ByName(kStmtParamNameIndexId, mIndexId))); + + if (!mUnique || !mIsLastIndex) { + QM_TRY(MOZ_TO_RESULT(selectStmt->BindInt64ByName( + kStmtParamNameObjectStoreId, mObjectStoreId))); + } + + Key lastObjectStoreKey; + IndexDataValuesAutoArray lastIndexValues; + + QM_TRY(CollectWhileHasResult( + *selectStmt, + [this, &aConnection, &lastObjectStoreKey, &lastIndexValues, + deleteIndexRowStmt = + DatabaseConnection::LazyStatement{ + *aConnection, + mUnique + ? "DELETE FROM unique_index_data " + "WHERE index_id = :"_ns + + kStmtParamNameIndexId + " AND value = :"_ns + + kStmtParamNameValue + ";"_ns + : "DELETE FROM index_data " + "WHERE index_id = :"_ns + + kStmtParamNameIndexId + " AND value = :"_ns + + kStmtParamNameValue + " AND object_data_key = :"_ns + + kStmtParamNameObjectDataKey + ";"_ns}]( + auto& selectStmt) mutable -> Result<Ok, nsresult> { + // We always need the index key to delete the index row. + Key indexKey; + QM_TRY(MOZ_TO_RESULT(indexKey.SetFromStatement(&selectStmt, 0))); + + QM_TRY(OkIf(!indexKey.IsUnset()), Err(NS_ERROR_FILE_CORRUPTED), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + // 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; + QM_TRY(MOZ_TO_RESULT(selectStmt.GetSharedBlob( + 1, &objectStoreKeyDataLength, &objectStoreKeyData))); + + QM_TRY(OkIf(objectStoreKeyDataLength), Err(NS_ERROR_FILE_CORRUPTED), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + const 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. + QM_TRY(MOZ_TO_RESULT(RemoveReferencesToIndex( + aConnection, lastObjectStoreKey, lastIndexValues))); + } + + // 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(); + QM_TRY(MOZ_TO_RESULT( + ReadCompressedIndexDataValues(selectStmt, 2, lastIndexValues))); + + QM_TRY(OkIf(!lastIndexValues.IsEmpty()), + Err(NS_ERROR_FILE_CORRUPTED), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + } + } + + // Now delete the index row. + { + QM_TRY_INSPECT(const auto& borrowedDeleteIndexRowStmt, + deleteIndexRowStmt.Borrow()); + + QM_TRY(MOZ_TO_RESULT(borrowedDeleteIndexRowStmt->BindInt64ByName( + kStmtParamNameIndexId, mIndexId))); + + QM_TRY(MOZ_TO_RESULT(indexKey.BindToStatement( + &*borrowedDeleteIndexRowStmt, kStmtParamNameValue))); + + if (!mUnique) { + QM_TRY(MOZ_TO_RESULT(lastObjectStoreKey.BindToStatement( + &*borrowedDeleteIndexRowStmt, kStmtParamNameObjectDataKey))); + } + + QM_TRY(MOZ_TO_RESULT(borrowedDeleteIndexRowStmt->Execute())); + } + + return Ok{}; + })); + + // Take care of the last key. + if (!lastObjectStoreKey.IsUnset()) { + MOZ_ASSERT_IF(!mIsLastIndex, !lastIndexValues.IsEmpty()); + + QM_TRY(MOZ_TO_RESULT(RemoveReferencesToIndex( + aConnection, lastObjectStoreKey, lastIndexValues))); + } + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM object_store_index " + "WHERE id = :index_id;"_ns, + [indexId = + mIndexId](mozIStorageStatement& deleteStmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(deleteStmt.BindInt64ByIndex(0, indexId))); + + return Ok{}; + }))); + +#ifdef DEBUG + { + int32_t deletedRowCount; + MOZ_ALWAYS_SUCCEEDS(aConnection->MutableStorageConnection().GetAffectedRows( + &deletedRowCount)); + MOZ_ASSERT(deletedRowCount == 1); + } +#endif + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +nsresult RenameIndexOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("RenameIndexOp::DoDatabaseWork", DOM); + +#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... + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY_INSPECT(const bool& hasResult, + aConnection + ->BorrowAndExecuteSingleStepStatement( + "SELECT name " + "FROM object_store_index " + "WHERE object_store_id = :object_store_id " + "AND name = :name " + "AND id != :id;"_ns, + [&self = *this](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex( + 0, self.mObjectStoreId))); + QM_TRY(MOZ_TO_RESULT( + stmt.BindStringByIndex(1, self.mNewName))); + QM_TRY(MOZ_TO_RESULT( + stmt.BindInt64ByIndex(2, self.mIndexId))); + + return Ok{}; + }) + .map(IsSome), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(!hasResult); + } +#else + Unused << mObjectStoreId; +#endif + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "UPDATE object_store_index " + "SET name = :name " + "WHERE id = :id;"_ns, + [&self = *this](mozIStorageStatement& stmt) -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindStringByIndex(0, self.mNewName))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByIndex(1, self.mIndexId))); + + return Ok{}; + }))); + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +Result<bool, nsresult> NormalTransactionOp::ObjectStoreHasIndexes( + DatabaseConnection& aConnection, const IndexOrObjectStoreId aObjectStoreId, + const bool aMayHaveIndexes) { + aConnection.AssertIsOnConnectionThread(); + MOZ_ASSERT(aObjectStoreId); + + if (Transaction().GetMode() == IDBTransaction::Mode::VersionChange && + 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. + QM_TRY_RETURN(DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, + aObjectStoreId)); + } + +#ifdef DEBUG + QM_TRY_INSPECT( + const bool& hasIndexes, + DatabaseOperationBase::ObjectStoreHasIndexes(aConnection, aObjectStoreId), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(aMayHaveIndexes == hasIndexes); +#endif + + return aMayHaveIndexes; +} + +Result<PreprocessParams, nsresult> NormalTransactionOp::GetPreprocessParams() { + return PreprocessParams{}; +} + +nsresult NormalTransactionOp::SendPreprocessInfo() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!IsActorDestroyed()); + + QM_TRY_INSPECT(const auto& params, GetPreprocessParams()); + + 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()) { + static const size_t kMaxIDBMsgOverhead = 1024 * 1024 * 10; // 10MB + const uint32_t maximalSizeFromPref = + IndexedDatabaseManager::MaxSerializedMsgSize(); + MOZ_ASSERT(maximalSizeFromPref > kMaxIDBMsgOverhead); + const size_t kMaxMessageSize = maximalSizeFromPref - kMaxIDBMsgOverhead; + + RequestResponse response; + size_t responseSize = kMaxMessageSize; + GetResponse(response, &responseSize); + + if (responseSize >= kMaxMessageSize) { + nsPrintfCString warning( + "The serialized value is too large" + " (size=%zu bytes, max=%zu bytes).", + responseSize, kMaxMessageSize); + NS_WARNING(warning.get()); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + 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(); + + // Assume ActorDestroy can happen at any time, so we can't probe the current + // state since mInternalState can be modified on any thread (only one thread + // at a time based on the state machine). + // However we can use mWaitingForContinue which is only touched on the owning + // thread. If mWaitingForContinue is true, we can also modify mInternalState + // since we are guaranteed that there are no pending runnables which would + // probe mInternalState to decide what code needs to run (there shouldn't be + // any running runnables on other threads either). + + if (IsWaitingForContinue()) { + NoteContinueReceived(); + } + + // We don't have to handle the case when mWaitingForContinue is not true since + // it means that either nothing has been initialized yet, so nothing to + // cleanup or there are pending runnables that will detect that the actor has + // been destroyed and cleanup accordingly. +} + +mozilla::ipc::IPCResult NormalTransactionOp::RecvContinue( + const PreprocessResponse& aResponse) { + AssertIsOnOwningThread(); + + switch (aResponse.type()) { + case PreprocessResponse::Tnsresult: + SetFailureCode(aResponse.get_nsresult()); + break; + + case PreprocessResponse::TObjectStoreGetPreprocessResponse: + case PreprocessResponse::TObjectStoreGetAllPreprocessResponse: + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + NoteContinueReceived(); + + return IPC_OK(); +} + +ObjectStoreAddOrPutRequestOp::ObjectStoreAddOrPutRequestOp( + SafeRefPtr<TransactionBase> aTransaction, RequestParams&& aParams) + : NormalTransactionOp(std::move(aTransaction)), + mParams( + std::move(aParams.type() == RequestParams::TObjectStoreAddParams + ? aParams.get_ObjectStoreAddParams().commonParams() + : aParams.get_ObjectStorePutParams().commonParams())), + mOriginMetadata(Transaction().GetDatabase().OriginMetadata()), + mPersistenceType(Transaction().GetDatabase().Type()), + mOverwrite(aParams.type() == RequestParams::TObjectStorePutParams), + mObjectStoreMayHaveIndexes(false) { + MOZ_ASSERT(aParams.type() == RequestParams::TObjectStoreAddParams || + aParams.type() == RequestParams::TObjectStorePutParams); + + mMetadata = + Transaction().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 + { + QM_TRY_INSPECT(const bool& hasIndexes, + DatabaseOperationBase::ObjectStoreHasIndexes( + *aConnection, mParams.objectStoreId()), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(hasIndexes, + "Don't use this slow method if there are no indexes!"); + } +#endif + + QM_TRY_INSPECT( + const auto& indexValuesStmt, + aConnection->BorrowAndExecuteSingleStepStatement( + "SELECT index_data_values " + "FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + " AND key = :"_ns + + kStmtParamNameKey + ";"_ns, + [&self = *this](auto& stmt) -> mozilla::Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( + kStmtParamNameObjectStoreId, self.mParams.objectStoreId()))); + + QM_TRY(MOZ_TO_RESULT( + self.mResponse.BindToStatement(&stmt, kStmtParamNameKey))); + + return Ok{}; + })); + + if (indexValuesStmt) { + QM_TRY_INSPECT(const auto& existingIndexValues, + ReadCompressedIndexDataValues(**indexValuesStmt, 0)); + + QM_TRY(MOZ_TO_RESULT( + DeleteIndexDataTableRows(aConnection, mResponse, existingIndexValues))); + } + + return NS_OK; +} + +bool ObjectStoreAddOrPutRequestOp::Init(TransactionBase& aTransaction) { + AssertIsOnOwningThread(); + + const nsTArray<IndexUpdateInfo>& indexUpdateInfos = + mParams.indexUpdateInfos(); + + if (!indexUpdateInfos.IsEmpty()) { + mUniqueIndexTable.emplace(); + + for (const auto& updateInfo : indexUpdateInfos) { + auto indexMetadata = mMetadata->mIndexes.Lookup(updateInfo.indexId()); + MOZ_ALWAYS_TRUE(indexMetadata); + + MOZ_ASSERT(!(*indexMetadata)->mDeleted); + + const IndexOrObjectStoreId& indexId = + (*indexMetadata)->mCommonMetadata.id(); + const bool& unique = (*indexMetadata)->mCommonMetadata.unique(); + + MOZ_ASSERT(indexId == updateInfo.indexId()); + MOZ_ASSERT_IF(!(*indexMetadata)->mCommonMetadata.multiEntry(), + !mUniqueIndexTable.ref().Contains(indexId)); + + if (NS_WARN_IF(!mUniqueIndexTable.ref().InsertOrUpdate(indexId, unique, + fallible))) { + return false; + } + } + } else if (mOverwrite) { + mUniqueIndexTable.emplace(); + } + + if (mUniqueIndexTable.isSome()) { + mUniqueIndexTable.ref().MarkImmutable(); + } + + QM_TRY_UNWRAP( + mStoredFileInfos, + TransformIntoNewArray( + mParams.fileAddInfos(), + [](const auto& fileAddInfo) { + MOZ_ASSERT(fileAddInfo.type() == StructuredCloneFileBase::eBlob || + fileAddInfo.type() == + StructuredCloneFileBase::eMutableFile); + + switch (fileAddInfo.type()) { + case StructuredCloneFileBase::eBlob: { + PBackgroundIDBDatabaseFileParent* file = + fileAddInfo.file().AsParent(); + MOZ_ASSERT(file); + + auto* const fileActor = static_cast<DatabaseFile*>(file); + MOZ_ASSERT(fileActor); + + return StoredFileInfo::CreateForBlob( + fileActor->GetFileInfoPtr(), fileActor); + } + + default: + MOZ_CRASH("Should never get here!"); + } + }, + fallible), + false); + + if (mDataOverThreshold) { + auto fileInfo = + aTransaction.GetDatabase().GetFileManager().CreateFileInfo(); + if (NS_WARN_IF(!fileInfo)) { + return false; + } + + mStoredFileInfos.EmplaceBack(StoredFileInfo::CreateForStructuredClone( + std::move(fileInfo), + MakeRefPtr<SCInputStream>(mParams.cloneInfo().data().data))); + } + + return true; +} + +nsresult ObjectStoreAddOrPutRequestOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection->HasStorageConnection()); + + AUTO_PROFILER_LABEL("ObjectStoreAddOrPutRequestOp::DoDatabaseWork", DOM); + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + QM_TRY_INSPECT(const bool& objectStoreHasIndexes, + ObjectStoreHasIndexes(*aConnection, mParams.objectStoreId(), + mObjectStoreMayHaveIndexes)); + + // This will be the final key we use. + Key& key = mResponse; + key = mParams.key(); + + const bool keyUnset = key.IsUnset(); + const IndexOrObjectStoreId osid = mParams.objectStoreId(); + + // First delete old index_data_values if we're overwriting something and we + // have indexes. + if (mOverwrite && !keyUnset && objectStoreHasIndexes) { + QM_TRY(MOZ_TO_RESULT(RemoveOldIndexDataValues(aConnection))); + } + + int64_t autoIncrementNum = 0; + + { + // 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. + const auto optReplaceDirective = + (!mOverwrite || keyUnset) ? ""_ns : "OR REPLACE "_ns; + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement( + "INSERT "_ns + optReplaceDirective + + "INTO object_data " + "(object_store_id, key, file_ids, data) " + "VALUES (:"_ns + + kStmtParamNameObjectStoreId + ", :"_ns + + kStmtParamNameKey + ", :"_ns + kStmtParamNameFileIds + + ", :"_ns + kStmtParamNameData + ");"_ns)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameObjectStoreId, osid))); + + const SerializedStructuredCloneWriteInfo& cloneInfo = mParams.cloneInfo(); + const JSStructuredCloneData& cloneData = cloneInfo.data().data; + const size_t cloneDataSize = cloneData.Size(); + + MOZ_ASSERT(!keyUnset || mMetadata->mCommonMetadata.autoIncrement(), + "Should have key unless autoIncrement"); + + if (mMetadata->mCommonMetadata.autoIncrement()) { + if (keyUnset) { + { + const auto&& lockedAutoIncrementIds = + mMetadata->mAutoIncrementIds.Lock(); + + autoIncrementNum = lockedAutoIncrementIds->next; + } + + MOZ_ASSERT(autoIncrementNum > 0); + + if (autoIncrementNum > (1LL << 53)) { + return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR; + } + + QM_TRY(key.SetFromInteger(autoIncrementNum)); + } else if (key.IsFloat()) { + double numericKey = key.ToFloat(); + numericKey = std::min(numericKey, double(1LL << 53)); + numericKey = floor(numericKey); + + const auto&& lockedAutoIncrementIds = + mMetadata->mAutoIncrementIds.Lock(); + if (numericKey >= lockedAutoIncrementIds->next) { + autoIncrementNum = numericKey; + } + } + + 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.Start(); + MOZ_ALWAYS_TRUE(cloneData.Advance(iter, cloneInfo.offsetToKeyProp())); + MOZ_ALWAYS_TRUE( + cloneData.UpdateBytes(iter, keyPropBuffer, keyPropSize)); + } + } + + key.BindToStatement(&*stmt, kStmtParamNameKey); + + 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; + + const uint32_t index = mStoredFileInfos.Length() - 1; + + const int64_t data = (uint64_t(flags) << 32) | index; + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameData, data))); + } else { + AutoTArray<char, 4096> flatCloneData; // 4096 from JSStructuredCloneData + QM_TRY(OkIf(flatCloneData.SetLength(cloneDataSize, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + { + auto iter = cloneData.Start(); + MOZ_ALWAYS_TRUE( + cloneData.ReadBytes(iter, flatCloneData.Elements(), cloneDataSize)); + } + + // Compress the bytes before adding into the database. + const char* const uncompressed = flatCloneData.Elements(); + const 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* const dataBuffer = + reinterpret_cast<uint8_t*>(compressed.release()); + const size_t dataBufferLength = compressedLength; + + QM_TRY(MOZ_TO_RESULT(stmt->BindAdoptedBlobByName( + kStmtParamNameData, dataBuffer, dataBufferLength))); + } + + 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 (auto& storedFileInfo : mStoredFileInfos) { + MOZ_ASSERT(storedFileInfo.IsValid()); + + QM_TRY_INSPECT(const auto& inputStream, + storedFileInfo.GetInputStream()); + + if (inputStream) { + if (fileHelper.isNothing()) { + fileHelper.emplace(Transaction().GetDatabase().GetFileManagerPtr()); + QM_TRY(MOZ_TO_RESULT(fileHelper->Init()), + NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + } + + const DatabaseFileInfo& fileInfo = storedFileInfo.GetFileInfo(); + const DatabaseFileManager& fileManager = fileInfo.Manager(); + + const auto file = fileHelper->GetFile(fileInfo); + QM_TRY(OkIf(file), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + const auto journalFile = fileHelper->GetJournalFile(fileInfo); + QM_TRY(OkIf(journalFile), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + nsCString fileKeyId; + fileKeyId.AppendInt(fileInfo.Id()); + + const auto maybeKey = + fileManager.IsInPrivateBrowsingMode() + ? fileManager.MutableCipherKeyManagerRef().Get(fileKeyId) + : Nothing(); + + QM_TRY(MOZ_TO_RESULT(fileHelper->CreateFileFromStream( + *file, *journalFile, *inputStream, + storedFileInfo.ShouldCompress(), maybeKey)) + .mapErr([](const nsresult rv) { + if (NS_ERROR_GET_MODULE(rv) != + NS_ERROR_MODULE_DOM_INDEXEDDB) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + return rv; + }), + QM_PROPAGATE, + ([&fileManager, &file = *file, + &journalFile = *journalFile](const auto) { + // Try to remove the file if the copy failed. + QM_TRY(MOZ_TO_RESULT( + fileManager.SyncDeleteFile(file, journalFile)), + QM_VOID); + })); + + storedFileInfo.NotifyWriteSucceeded(); + } + + if (!fileIds.IsEmpty()) { + fileIds.Append(' '); + } + storedFileInfo.Serialize(fileIds); + } + + QM_TRY(MOZ_TO_RESULT( + stmt->BindStringByName(kStmtParamNameFileIds, fileIds))); + } else { + QM_TRY(MOZ_TO_RESULT(stmt->BindNullByName(kStmtParamNameFileIds))); + } + + QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_PROPAGATE, + [keyUnset = DebugOnly{keyUnset}](const nsresult rv) { + if (rv == NS_ERROR_STORAGE_CONSTRAINT) { + MOZ_ASSERT(!keyUnset, "Generated key had a collision!"); + } + }); + } + + // Update our indexes if needed. + if (!mParams.indexUpdateInfos().IsEmpty()) { + MOZ_ASSERT(mUniqueIndexTable.isSome()); + + // Write the index_data_values column. + QM_TRY_INSPECT(const auto& indexValues, + IndexDataValuesFromUpdateInfos(mParams.indexUpdateInfos(), + mUniqueIndexTable.ref())); + + QM_TRY( + MOZ_TO_RESULT(UpdateIndexValues(aConnection, osid, key, indexValues))); + + QM_TRY(MOZ_TO_RESULT( + InsertIndexTableRows(aConnection, osid, key, indexValues))); + } + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + if (autoIncrementNum) { + { + auto&& lockedAutoIncrementIds = mMetadata->mAutoIncrementIds.Lock(); + + lockedAutoIncrementIds->next = autoIncrementNum + 1; + } + + Transaction().NoteModifiedAutoIncrementObjectStore(mMetadata); + } + + return NS_OK; +} + +void ObjectStoreAddOrPutRequestOp::GetResponse(RequestResponse& aResponse, + size_t* aResponseSize) { + AssertIsOnOwningThread(); + + if (mOverwrite) { + aResponse = ObjectStorePutResponse(mResponse); + *aResponseSize = mResponse.GetBuffer().Length(); + } else { + aResponse = ObjectStoreAddResponse(mResponse); + *aResponseSize = mResponse.GetBuffer().Length(); + } +} + +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::StreamStatus() { return NS_OK; } + +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; + + if (NS_WARN_IF(!mData.Advance(mIter, count))) { + // InputStreams do not propagate errors to caller. + return NS_OK; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +ObjectStoreAddOrPutRequestOp::SCInputStream::IsNonBlocking(bool* _retval) { + *_retval = false; + return NS_OK; +} + +ObjectStoreGetRequestOp::ObjectStoreGetRequestOp( + SafeRefPtr<TransactionBase> aTransaction, const RequestParams& aParams, + bool aGetAll) + : NormalTransactionOp(std::move(aTransaction)), + mObjectStoreId(aGetAll + ? aParams.get_ObjectStoreGetAllParams().objectStoreId() + : aParams.get_ObjectStoreGetParams().objectStoreId()), + mDatabase(Transaction().GetDatabasePtr()), + mOptionalKeyRange( + aGetAll ? aParams.get_ObjectStoreGetAllParams().optionalKeyRange() + : Some(aParams.get_ObjectStoreGetParams().keyRange())), + mBackgroundParent(Transaction().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.isSome()); + MOZ_ASSERT(mBackgroundParent); +} + +template <typename T> +Result<T, nsresult> ObjectStoreGetRequestOp::ConvertResponse( + StructuredCloneReadInfoParent&& aInfo) { + T result; + + static_assert(std::is_same_v<T, SerializedStructuredCloneReadInfo> || + std::is_same_v<T, PreprocessInfo>); + + if constexpr (std::is_same_v<T, SerializedStructuredCloneReadInfo>) { + result.data().data = aInfo.ReleaseData(); + result.hasPreprocessInfo() = aInfo.HasPreprocessInfo(); + } + + QM_TRY_UNWRAP(result.files(), SerializeStructuredCloneFiles( + mDatabase, aInfo.Files(), + std::is_same_v<T, PreprocessInfo>)); + + return result; +} + +nsresult ObjectStoreGetRequestOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.isSome()); + MOZ_ASSERT_IF(!mGetAll, mLimit == 1); + + AUTO_PROFILER_LABEL("ObjectStoreGetRequestOp::DoDatabaseWork", DOM); + + const nsCString query = + "SELECT file_ids, data " + "FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameKey) + + " ORDER BY key ASC"_ns + + (mLimit ? kOpenLimit + IntToCString(mLimit) : EmptyCString()); + + QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(query)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameObjectStoreId, mObjectStoreId))); + + if (mOptionalKeyRange.isSome()) { + QM_TRY(MOZ_TO_RESULT( + BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt))); + } + + QM_TRY(CollectWhileHasResult( + *stmt, [this](auto& stmt) mutable -> mozilla::Result<Ok, nsresult> { + QM_TRY_UNWRAP(auto cloneInfo, + GetStructuredCloneReadInfoFromStatement( + &stmt, 1, 0, mDatabase->GetFileManager())); + + if (cloneInfo.HasPreprocessInfo()) { + mPreprocessInfoCount++; + } + + QM_TRY(OkIf(mResponse.EmplaceBack(fallible, std::move(cloneInfo))), + Err(NS_ERROR_OUT_OF_MEMORY)); + + return Ok{}; + })); + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +bool ObjectStoreGetRequestOp::HasPreprocessInfo() { + return mPreprocessInfoCount > 0; +} + +Result<PreprocessParams, nsresult> +ObjectStoreGetRequestOp::GetPreprocessParams() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mResponse.IsEmpty()); + + if (mGetAll) { + auto params = ObjectStoreGetAllPreprocessParams(); + + auto& preprocessInfos = params.preprocessInfos(); + if (NS_WARN_IF( + !preprocessInfos.SetCapacity(mPreprocessInfoCount, fallible))) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + QM_TRY(TransformIfAbortOnErr( + std::make_move_iterator(mResponse.begin()), + std::make_move_iterator(mResponse.end()), + MakeBackInserter(preprocessInfos), + [](const auto& info) { return info.HasPreprocessInfo(); }, + [&self = *this](StructuredCloneReadInfoParent&& info) { + return self.ConvertResponse<PreprocessInfo>(std::move(info)); + })); + + return PreprocessParams{std::move(params)}; + } + + auto params = ObjectStoreGetPreprocessParams(); + + QM_TRY_UNWRAP(params.preprocessInfo(), + ConvertResponse<PreprocessInfo>(std::move(mResponse[0]))); + + return PreprocessParams{std::move(params)}; +} + +void ObjectStoreGetRequestOp::GetResponse(RequestResponse& aResponse, + size_t* aResponseSize) { + MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); + + if (mGetAll) { + aResponse = ObjectStoreGetAllResponse(); + *aResponseSize = 0; + + if (!mResponse.IsEmpty()) { + QM_TRY_UNWRAP( + aResponse.get_ObjectStoreGetAllResponse().cloneInfos(), + TransformIntoNewArrayAbortOnErr( + std::make_move_iterator(mResponse.begin()), + std::make_move_iterator(mResponse.end()), + [this, &aResponseSize](StructuredCloneReadInfoParent&& info) { + *aResponseSize += info.Size(); + return ConvertResponse<SerializedStructuredCloneReadInfo>( + std::move(info)); + }, + fallible), + QM_VOID, [&aResponse](const nsresult result) { aResponse = result; }); + } + + return; + } + + aResponse = ObjectStoreGetResponse(); + *aResponseSize = 0; + + if (!mResponse.IsEmpty()) { + SerializedStructuredCloneReadInfo& serializedInfo = + aResponse.get_ObjectStoreGetResponse().cloneInfo(); + + *aResponseSize += mResponse[0].Size(); + QM_TRY_UNWRAP(serializedInfo, + ConvertResponse<SerializedStructuredCloneReadInfo>( + std::move(mResponse[0])), + QM_VOID, + [&aResponse](const nsresult result) { aResponse = result; }); + } +} + +ObjectStoreGetKeyRequestOp::ObjectStoreGetKeyRequestOp( + SafeRefPtr<TransactionBase> aTransaction, const RequestParams& aParams, + bool aGetAll) + : NormalTransactionOp(std::move(aTransaction)), + mObjectStoreId( + aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().objectStoreId() + : aParams.get_ObjectStoreGetKeyParams().objectStoreId()), + mOptionalKeyRange( + aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().optionalKeyRange() + : Some(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.isSome()); +} + +nsresult ObjectStoreGetKeyRequestOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("ObjectStoreGetKeyRequestOp::DoDatabaseWork", DOM); + + const nsCString query = + "SELECT key " + "FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameKey) + + " ORDER BY key ASC"_ns + + (mLimit ? " LIMIT "_ns + IntToCString(mLimit) : EmptyCString()); + + QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(query)); + + nsresult rv = + stmt->BindInt64ByName(kStmtParamNameObjectStoreId, mObjectStoreId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mOptionalKeyRange.isSome()) { + rv = BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + QM_TRY(CollectWhileHasResult( + *stmt, [this](auto& stmt) mutable -> mozilla::Result<Ok, nsresult> { + Key* const key = mResponse.AppendElement(fallible); + QM_TRY(OkIf(key), Err(NS_ERROR_OUT_OF_MEMORY)); + QM_TRY(MOZ_TO_RESULT(key->SetFromStatement(&stmt, 0))); + + return Ok{}; + })); + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +void ObjectStoreGetKeyRequestOp::GetResponse(RequestResponse& aResponse, + size_t* aResponseSize) { + MOZ_ASSERT_IF(mLimit, mResponse.Length() <= mLimit); + + if (mGetAll) { + aResponse = ObjectStoreGetAllKeysResponse(); + *aResponseSize = std::accumulate(mResponse.begin(), mResponse.end(), 0u, + [](size_t old, const auto& entry) { + return old + entry.GetBuffer().Length(); + }); + + aResponse.get_ObjectStoreGetAllKeysResponse().keys() = std::move(mResponse); + + return; + } + + aResponse = ObjectStoreGetKeyResponse(); + *aResponseSize = 0; + + if (!mResponse.IsEmpty()) { + *aResponseSize = mResponse[0].GetBuffer().Length(); + aResponse.get_ObjectStoreGetKeyResponse().key() = std::move(mResponse[0]); + } +} + +ObjectStoreDeleteRequestOp::ObjectStoreDeleteRequestOp( + SafeRefPtr<TransactionBase> aTransaction, + const ObjectStoreDeleteParams& aParams) + : NormalTransactionOp(std::move(aTransaction)), + mParams(aParams), + mObjectStoreMayHaveIndexes(false) { + AssertIsOnBackgroundThread(); + + SafeRefPtr<FullObjectStoreMetadata> metadata = + Transaction().GetMetadataForObjectStoreId(mParams.objectStoreId()); + MOZ_ASSERT(metadata); + + mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); +} + +nsresult ObjectStoreDeleteRequestOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + AUTO_PROFILER_LABEL("ObjectStoreDeleteRequestOp::DoDatabaseWork", DOM); + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + QM_TRY_INSPECT(const bool& objectStoreHasIndexes, + ObjectStoreHasIndexes(*aConnection, mParams.objectStoreId(), + mObjectStoreMayHaveIndexes)); + + if (objectStoreHasIndexes) { + QM_TRY(MOZ_TO_RESULT(DeleteObjectStoreDataTableRowsWithIndexes( + aConnection, mParams.objectStoreId(), Some(mParams.keyRange())))); + } else { + const auto keyRangeClause = + GetBindingClauseForKeyRange(mParams.keyRange(), kColumnNameKey); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement( + "DELETE FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + keyRangeClause + ";"_ns, + [¶ms = mParams]( + mozIStorageStatement& stmt) -> mozilla::Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName(kStmtParamNameObjectStoreId, + params.objectStoreId()))); + + QM_TRY( + MOZ_TO_RESULT(BindKeyRangeToStatement(params.keyRange(), &stmt))); + + return Ok{}; + }))); + } + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +ObjectStoreClearRequestOp::ObjectStoreClearRequestOp( + SafeRefPtr<TransactionBase> aTransaction, + const ObjectStoreClearParams& aParams) + : NormalTransactionOp(std::move(aTransaction)), + mParams(aParams), + mObjectStoreMayHaveIndexes(false) { + AssertIsOnBackgroundThread(); + + SafeRefPtr<FullObjectStoreMetadata> metadata = + Transaction().GetMetadataForObjectStoreId(mParams.objectStoreId()); + MOZ_ASSERT(metadata); + + mObjectStoreMayHaveIndexes = metadata->HasLiveIndexes(); +} + +nsresult ObjectStoreClearRequestOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("ObjectStoreClearRequestOp::DoDatabaseWork", DOM); + + DatabaseConnection::AutoSavepoint autoSave; + QM_TRY(MOZ_TO_RESULT(autoSave.Start(Transaction())) +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + , + QM_PROPAGATE, MakeAutoSavepointCleanupHandler(*aConnection) +#endif + ); + + QM_TRY_INSPECT(const bool& objectStoreHasIndexes, + ObjectStoreHasIndexes(*aConnection, mParams.objectStoreId(), + mObjectStoreMayHaveIndexes)); + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + QM_TRY(MOZ_TO_RESULT( + objectStoreHasIndexes + ? DeleteObjectStoreDataTableRowsWithIndexes( + aConnection, mParams.objectStoreId(), Nothing()) + : aConnection->ExecuteCachedStatement( + "DELETE FROM object_data " + "WHERE object_store_id = :object_store_id;"_ns, + [objectStoreId = + mParams.objectStoreId()](mozIStorageStatement& stmt) + -> mozilla::Result<Ok, nsresult> { + QM_TRY( + MOZ_TO_RESULT(stmt.BindInt64ByIndex(0, objectStoreId))); + + return Ok{}; + }))); + + QM_TRY(MOZ_TO_RESULT(autoSave.Commit())); + + return NS_OK; +} + +nsresult ObjectStoreCountRequestOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("ObjectStoreCountRequestOp::DoDatabaseWork", DOM); + + const auto keyRangeClause = MaybeGetBindingClauseForKeyRange( + mParams.optionalKeyRange(), kColumnNameKey); + + QM_TRY_INSPECT( + const auto& maybeStmt, + aConnection->BorrowAndExecuteSingleStepStatement( + "SELECT count(*) " + "FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameObjectStoreId + keyRangeClause, + [¶ms = mParams](auto& stmt) -> mozilla::Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( + kStmtParamNameObjectStoreId, params.objectStoreId()))); + + if (params.optionalKeyRange().isSome()) { + QM_TRY(MOZ_TO_RESULT(BindKeyRangeToStatement( + params.optionalKeyRange().ref(), &stmt))); + } + + return Ok{}; + })); + + QM_TRY(OkIf(maybeStmt.isSome()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + [](const auto) { + // XXX Why do we have an assertion here, but not at most other + // places using IDB_REPORT_INTERNAL_ERR(_LAMBDA)? + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + }); + + const auto& stmt = *maybeStmt; + + const int64_t count = stmt->AsInt64(0); + QM_TRY(OkIf(count >= 0), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, [](const auto) { + // XXX Why do we have an assertion here, but not at most other places using + // IDB_REPORT_INTERNAL_ERR(_LAMBDA)? + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + }); + + mResponse.count() = count; + + return NS_OK; +} + +// static +SafeRefPtr<FullIndexMetadata> IndexRequestOpBase::IndexMetadataForParams( + const TransactionBase& aTransaction, const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TIndexGetParams || + aParams.type() == RequestParams::TIndexGetKeyParams || + aParams.type() == RequestParams::TIndexGetAllParams || + aParams.type() == RequestParams::TIndexGetAllKeysParams || + aParams.type() == RequestParams::TIndexCountParams); + + IndexOrObjectStoreId objectStoreId; + IndexOrObjectStoreId 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 SafeRefPtr<FullObjectStoreMetadata> objectStoreMetadata = + aTransaction.GetMetadataForObjectStoreId(objectStoreId); + MOZ_ASSERT(objectStoreMetadata); + + SafeRefPtr<FullIndexMetadata> indexMetadata = + aTransaction.GetMetadataForIndexId(*objectStoreMetadata, indexId); + MOZ_ASSERT(indexMetadata); + + return indexMetadata; +} + +IndexGetRequestOp::IndexGetRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const RequestParams& aParams, bool aGetAll) + : IndexRequestOpBase(std::move(aTransaction), aParams), + mDatabase(Transaction().GetDatabasePtr()), + mOptionalKeyRange(aGetAll + ? aParams.get_IndexGetAllParams().optionalKeyRange() + : Some(aParams.get_IndexGetParams().keyRange())), + mBackgroundParent(Transaction().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.isSome()); + MOZ_ASSERT(mBackgroundParent); +} + +nsresult IndexGetRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.isSome()); + MOZ_ASSERT_IF(!mGetAll, mLimit == 1); + + AUTO_PROFILER_LABEL("IndexGetRequestOp::DoDatabaseWork", DOM); + + const auto indexTable = mMetadata->mCommonMetadata.unique() + ? "unique_index_data "_ns + : "index_data "_ns; + + QM_TRY_INSPECT( + const auto& stmt, + aConnection->BorrowCachedStatement( + "SELECT file_ids, data " + "FROM object_data " + "INNER JOIN "_ns + + indexTable + + "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 = :"_ns + + kStmtParamNameIndexId + + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, + kColumnNameValue) + + (mLimit ? kOpenLimit + IntToCString(mLimit) : EmptyCString()))); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameIndexId, + mMetadata->mCommonMetadata.id()))); + + if (mOptionalKeyRange.isSome()) { + QM_TRY(MOZ_TO_RESULT( + BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt))); + } + + QM_TRY(CollectWhileHasResult( + *stmt, [this](auto& stmt) mutable -> mozilla::Result<Ok, nsresult> { + QM_TRY_UNWRAP(auto cloneInfo, + GetStructuredCloneReadInfoFromStatement( + &stmt, 1, 0, mDatabase->GetFileManager())); + + if (cloneInfo.HasPreprocessInfo()) { + IDB_WARNING("Preprocessing for indexes not yet implemented!"); + return Err(NS_ERROR_NOT_IMPLEMENTED); + } + + QM_TRY(OkIf(mResponse.EmplaceBack(fallible, std::move(cloneInfo))), + Err(NS_ERROR_OUT_OF_MEMORY)); + + return Ok{}; + })); + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +// XXX This is more or less a duplicate of ObjectStoreGetRequestOp::GetResponse +void IndexGetRequestOp::GetResponse(RequestResponse& aResponse, + size_t* aResponseSize) { + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + auto convertResponse = [this](StructuredCloneReadInfoParent&& info) + -> mozilla::Result<SerializedStructuredCloneReadInfo, nsresult> { + SerializedStructuredCloneReadInfo result; + + result.data().data = info.ReleaseData(); + + QM_TRY_UNWRAP(result.files(), SerializeStructuredCloneFiles( + mDatabase, info.Files(), false)); + + return result; + }; + + if (mGetAll) { + aResponse = IndexGetAllResponse(); + *aResponseSize = 0; + + if (!mResponse.IsEmpty()) { + QM_TRY_UNWRAP( + aResponse.get_IndexGetAllResponse().cloneInfos(), + TransformIntoNewArrayAbortOnErr( + std::make_move_iterator(mResponse.begin()), + std::make_move_iterator(mResponse.end()), + [convertResponse, + &aResponseSize](StructuredCloneReadInfoParent&& info) { + *aResponseSize += info.Size(); + return convertResponse(std::move(info)); + }, + fallible), + QM_VOID, [&aResponse](const nsresult result) { aResponse = result; }); + } + + return; + } + + aResponse = IndexGetResponse(); + *aResponseSize = 0; + + if (!mResponse.IsEmpty()) { + SerializedStructuredCloneReadInfo& serializedInfo = + aResponse.get_IndexGetResponse().cloneInfo(); + + *aResponseSize += mResponse[0].Size(); + QM_TRY_UNWRAP(serializedInfo, convertResponse(std::move(mResponse[0])), + QM_VOID, + [&aResponse](const nsresult result) { aResponse = result; }); + } +} + +IndexGetKeyRequestOp::IndexGetKeyRequestOp( + SafeRefPtr<TransactionBase> aTransaction, const RequestParams& aParams, + bool aGetAll) + : IndexRequestOpBase(std::move(aTransaction), aParams), + mOptionalKeyRange( + aGetAll ? aParams.get_IndexGetAllKeysParams().optionalKeyRange() + : Some(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.isSome()); +} + +nsresult IndexGetKeyRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(!mGetAll, mOptionalKeyRange.isSome()); + MOZ_ASSERT_IF(!mGetAll, mLimit == 1); + + AUTO_PROFILER_LABEL("IndexGetKeyRequestOp::DoDatabaseWork", DOM); + + const bool hasKeyRange = mOptionalKeyRange.isSome(); + + const auto indexTable = mMetadata->mCommonMetadata.unique() + ? "unique_index_data "_ns + : "index_data "_ns; + + const nsCString query = + "SELECT object_data_key " + "FROM "_ns + + indexTable + "WHERE index_id = :"_ns + kStmtParamNameIndexId + + MaybeGetBindingClauseForKeyRange(mOptionalKeyRange, kColumnNameValue) + + (mLimit ? kOpenLimit + IntToCString(mLimit) : EmptyCString()); + + QM_TRY_INSPECT(const auto& stmt, aConnection->BorrowCachedStatement(query)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameIndexId, + mMetadata->mCommonMetadata.id()))); + + if (hasKeyRange) { + QM_TRY(MOZ_TO_RESULT( + BindKeyRangeToStatement(mOptionalKeyRange.ref(), &*stmt))); + } + + QM_TRY(CollectWhileHasResult( + *stmt, [this](auto& stmt) mutable -> mozilla::Result<Ok, nsresult> { + Key* const key = mResponse.AppendElement(fallible); + QM_TRY(OkIf(key), Err(NS_ERROR_OUT_OF_MEMORY)); + QM_TRY(MOZ_TO_RESULT(key->SetFromStatement(&stmt, 0))); + + return Ok{}; + })); + + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + return NS_OK; +} + +void IndexGetKeyRequestOp::GetResponse(RequestResponse& aResponse, + size_t* aResponseSize) { + MOZ_ASSERT_IF(!mGetAll, mResponse.Length() <= 1); + + if (mGetAll) { + aResponse = IndexGetAllKeysResponse(); + *aResponseSize = std::accumulate(mResponse.begin(), mResponse.end(), 0u, + [](size_t old, const auto& entry) { + return old + entry.GetBuffer().Length(); + }); + + aResponse.get_IndexGetAllKeysResponse().keys() = std::move(mResponse); + + return; + } + + aResponse = IndexGetKeyResponse(); + *aResponseSize = 0; + + if (!mResponse.IsEmpty()) { + *aResponseSize = mResponse[0].GetBuffer().Length(); + aResponse.get_IndexGetKeyResponse().key() = std::move(mResponse[0]); + } +} + +nsresult IndexCountRequestOp::DoDatabaseWork(DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + + AUTO_PROFILER_LABEL("IndexCountRequestOp::DoDatabaseWork", DOM); + + const auto indexTable = mMetadata->mCommonMetadata.unique() + ? "unique_index_data "_ns + : "index_data "_ns; + + const auto keyRangeClause = MaybeGetBindingClauseForKeyRange( + mParams.optionalKeyRange(), kColumnNameValue); + + QM_TRY_INSPECT( + const auto& maybeStmt, + aConnection->BorrowAndExecuteSingleStepStatement( + "SELECT count(*) " + "FROM "_ns + + indexTable + "WHERE index_id = :"_ns + kStmtParamNameIndexId + + keyRangeClause, + [&self = *this](auto& stmt) -> mozilla::Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName( + kStmtParamNameIndexId, self.mMetadata->mCommonMetadata.id()))); + + if (self.mParams.optionalKeyRange().isSome()) { + QM_TRY(MOZ_TO_RESULT(BindKeyRangeToStatement( + self.mParams.optionalKeyRange().ref(), &stmt))); + } + + return Ok{}; + })); + + QM_TRY(OkIf(maybeStmt.isSome()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + [](const auto) { + // XXX Why do we have an assertion here, but not at most other + // places using IDB_REPORT_INTERNAL_ERR(_LAMBDA)? + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + }); + + const auto& stmt = *maybeStmt; + + const int64_t count = stmt->AsInt64(0); + QM_TRY(OkIf(count >= 0), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, [](const auto) { + // XXX Why do we have an assertion here, but not at most other places using + // IDB_REPORT_INTERNAL_ERR(_LAMBDA)? + MOZ_ASSERT(false, "This should never be possible!"); + IDB_REPORT_INTERNAL_ERR(); + }); + + mResponse.count() = count; + + return NS_OK; +} + +template <IDBCursorType CursorType> +bool Cursor<CursorType>::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. + // + // TODO: There seem to be other cases when mFiles is non-empty here, which + // have been present before adding cursor preloading, but with cursor + // preloading they have become more frequent (also during startup). One + // possible cause with cursor preloading is to be addressed by Bug 1597191. + NS_WARNING_ASSERTION( + !mFiles.IsEmpty() && !Transaction().IsInvalidated(), + "Expected empty mFiles when transaction has not been invalidated"); + + // SendResponseInternal will assert when mResponse.type() is + // CursorResponse::Tnsresult and mFiles is non-empty, so we clear mFiles + // here. + mFiles.Clear(); + + mCursor->SendResponseInternal(mResponse, mFiles); + } + +#ifdef DEBUG + mResponseSent = true; +#endif + return false; +} + +template <IDBCursorType CursorType> +void Cursor<CursorType>::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(); +} + +template <IDBCursorType CursorType> +ResponseSizeOrError +CursorOpBaseHelperBase<CursorType>::PopulateResponseFromStatement( + mozIStorageStatement* const aStmt, const bool aInitializeResponse, + Key* const aOptOutSortKey) { + mOp.Transaction().AssertIsOnConnectionThread(); + MOZ_ASSERT_IF(aInitializeResponse, + mOp.mResponse.type() == CursorResponse::T__None); + MOZ_ASSERT_IF(!aInitializeResponse, + mOp.mResponse.type() != CursorResponse::T__None); + MOZ_ASSERT_IF( + mOp.mFiles.IsEmpty() && + (mOp.mResponse.type() == + CursorResponse::TArrayOfObjectStoreCursorResponse || + mOp.mResponse.type() == CursorResponse::TArrayOfIndexCursorResponse), + aInitializeResponse); + + auto populateResponseHelper = PopulateResponseHelper<CursorType>{mOp}; + auto previousKey = aOptOutSortKey ? std::move(*aOptOutSortKey) : Key{}; + + QM_TRY(MOZ_TO_RESULT(populateResponseHelper.GetKeys(aStmt, aOptOutSortKey))); + + // aOptOutSortKey must be set iff the cursor is a unique cursor. For unique + // cursors, we need to skip records with the same key. The SQL queries + // currently do not filter these out. + if (aOptOutSortKey && !previousKey.IsUnset() && + previousKey == *aOptOutSortKey) { + return 0; + } + + QM_TRY(MOZ_TO_RESULT( + populateResponseHelper.MaybeGetCloneInfo(aStmt, GetCursor()))); + + // CAUTION: It is important that only the part of the function above this + // comment may fail, and modifications to the data structure (in particular + // mResponse and mFiles) may only be made below. This is necessary to allow to + // discard entries that were attempted to be preloaded without causing an + // inconsistent state. + + if (aInitializeResponse) { + mOp.mResponse = std::remove_reference_t< + decltype(populateResponseHelper.GetTypedResponse(&mOp.mResponse))>(); + } + + auto& responses = populateResponseHelper.GetTypedResponse(&mOp.mResponse); + auto& response = *responses.AppendElement(); + + populateResponseHelper.FillKeys(response); + if constexpr (!CursorTypeTraits<CursorType>::IsKeyOnlyCursor) { + populateResponseHelper.MaybeFillCloneInfo(response, &mOp.mFiles); + } + + return populateResponseHelper.GetKeySize(response) + + populateResponseHelper.MaybeGetCloneInfoSize(response); +} + +template <IDBCursorType CursorType> +void CursorOpBaseHelperBase<CursorType>::PopulateExtraResponses( + mozIStorageStatement* const aStmt, const uint32_t aMaxExtraCount, + const size_t aInitialResponseSize, const nsACString& aOperation, + Key* const aOptPreviousSortKey) { + mOp.AssertIsOnConnectionThread(); + + const auto extraCount = [&]() -> uint32_t { + auto accumulatedResponseSize = aInitialResponseSize; + uint32_t extraCount = 0; + + do { + bool hasResult; + nsresult rv = aStmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + // In case of a failure on one step, do not attempt to execute further + // steps, but use the results already populated. + + break; + } + + if (!hasResult) { + break; + } + + // PopulateResponseFromStatement does not modify the data in case of + // failure, so we can just use the results already populated, and discard + // any remaining entries, and signal overall success. Probably, future + // attempts to access the same entry will fail as well, but it might never + // be accessed by the application. + QM_TRY_INSPECT( + const auto& responseSize, + PopulateResponseFromStatement(aStmt, false, aOptPreviousSortKey), + extraCount, [](const auto&) { + // TODO: Maybe disable preloading for this cursor? The problem will + // probably reoccur on the next attempt, and disabling preloading + // will reduce latency. However, if some problematic entry will be + // skipped over, after that it might be fine again. To judge this, + // the causes for such failures would need to be analyzed more + // thoroughly. Since this seems to be rare, maybe no further action + // is necessary at all. + }); + + // Check accumulated size of individual responses and maybe break early. + accumulatedResponseSize += responseSize; + if (accumulatedResponseSize > IPC::Channel::kMaximumMessageSize / 2) { + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "PRELOAD: %s: Dropping entries because maximum message size is " + "exceeded: %" PRIu32 "/%zu bytes", + "%.0s Dropping too large (%" PRIu32 "/%zu)", + IDB_LOG_ID_STRING(mOp.mBackgroundChildLoggingId), + mOp.mTransactionLoggingSerialNumber, mOp.mLoggingSerialNumber, + PromiseFlatCString(aOperation).get(), extraCount, + accumulatedResponseSize); + + break; + } + + // TODO: Do not count entries skipped for unique cursors. + ++extraCount; + } while (true); + + return extraCount; + }(); + + IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( + "PRELOAD: %s: Number of extra results populated: %" PRIu32 "/%" PRIu32, + "%.0s Populated (%" PRIu32 "/%" PRIu32 ")", + IDB_LOG_ID_STRING(mOp.mBackgroundChildLoggingId), + mOp.mTransactionLoggingSerialNumber, mOp.mLoggingSerialNumber, + PromiseFlatCString(aOperation).get(), extraCount, aMaxExtraCount); +} + +template <IDBCursorType CursorType> +void Cursor<CursorType>::SetOptionalKeyRange( + const Maybe<SerializedKeyRange>& aOptionalKeyRange, bool* const aOpen) { + MOZ_ASSERT(aOpen); + + Key localeAwareRangeBound; + + if (aOptionalKeyRange.isSome()) { + const SerializedKeyRange& range = aOptionalKeyRange.ref(); + + const bool lowerBound = !IsIncreasingOrder(mDirection); + *aOpen = + !range.isOnly() && (lowerBound ? range.lowerOpen() : range.upperOpen()); + + const auto& bound = + (range.isOnly() || lowerBound) ? range.lower() : range.upper(); + if constexpr (IsIndexCursor) { + if (this->IsLocaleAware()) { + // XXX Don't we need to propagate the error? + QM_TRY_UNWRAP(localeAwareRangeBound, + bound.ToLocaleAwareKey(this->mLocale), QM_VOID); + } else { + localeAwareRangeBound = bound; + } + } else { + localeAwareRangeBound = bound; + } + } else { + *aOpen = false; + } + + this->mLocaleAwareRangeBound.init(std::move(localeAwareRangeBound)); +} + +template <IDBCursorType CursorType> +void ObjectStoreOpenOpHelper<CursorType>::PrepareKeyConditionClauses( + const nsACString& aDirectionClause, const nsACString& aQueryStart) { + const bool isIncreasingOrder = IsIncreasingOrder(GetCursor().mDirection); + + nsAutoCString keyRangeClause; + nsAutoCString continueToKeyRangeClause; + AppendConditionClause(kStmtParamNameKey, kStmtParamNameCurrentKey, + !isIncreasingOrder, false, keyRangeClause); + AppendConditionClause(kStmtParamNameKey, kStmtParamNameCurrentKey, + !isIncreasingOrder, true, continueToKeyRangeClause); + + { + bool open; + GetCursor().SetOptionalKeyRange(GetOptionalKeyRange(), &open); + + if (GetOptionalKeyRange().isSome() && + !GetCursor().mLocaleAwareRangeBound->IsUnset()) { + AppendConditionClause(kStmtParamNameKey, kStmtParamNameRangeBound, + isIncreasingOrder, !open, keyRangeClause); + AppendConditionClause(kStmtParamNameKey, kStmtParamNameRangeBound, + isIncreasingOrder, !open, continueToKeyRangeClause); + } + } + + const nsAutoCString suffix = + aDirectionClause + kOpenLimit + ":"_ns + kStmtParamNameLimit; + + GetCursor().mContinueQueries.init( + aQueryStart + keyRangeClause + suffix, + aQueryStart + continueToKeyRangeClause + suffix); +} + +template <IDBCursorType CursorType> +void IndexOpenOpHelper<CursorType>::PrepareIndexKeyConditionClause( + const nsACString& aDirectionClause, + const nsLiteralCString& aObjectDataKeyPrefix, nsAutoCString aQueryStart) { + const bool isIncreasingOrder = IsIncreasingOrder(GetCursor().mDirection); + + { + bool open; + GetCursor().SetOptionalKeyRange(GetOptionalKeyRange(), &open); + if (GetOptionalKeyRange().isSome() && + !GetCursor().mLocaleAwareRangeBound->IsUnset()) { + AppendConditionClause(kColumnNameAliasSortKey, kStmtParamNameRangeBound, + isIncreasingOrder, !open, aQueryStart); + } + } + + nsCString continueQuery, continueToQuery, continuePrimaryKeyQuery; + + continueToQuery = + aQueryStart + " AND "_ns + + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterOrEquals + : ComparisonOperator::LessOrEquals, + kStmtParamNameCurrentKey); + + switch (GetCursor().mDirection) { + case IDBCursorDirection::Next: + case IDBCursorDirection::Prev: + continueQuery = + aQueryStart + " AND "_ns + + GetSortKeyClause(isIncreasingOrder + ? ComparisonOperator::GreaterOrEquals + : ComparisonOperator::LessOrEquals, + kStmtParamNameCurrentKey) + + " AND ( "_ns + + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterThan + : ComparisonOperator::LessThan, + kStmtParamNameCurrentKey) + + " OR "_ns + + GetKeyClause(aObjectDataKeyPrefix + "object_data_key"_ns, + isIncreasingOrder ? ComparisonOperator::GreaterThan + : ComparisonOperator::LessThan, + kStmtParamNameObjectStorePosition) + + " ) "_ns; + + continuePrimaryKeyQuery = + aQueryStart + + " AND (" + "("_ns + + GetSortKeyClause(ComparisonOperator::Equals, + kStmtParamNameCurrentKey) + + " AND "_ns + + GetKeyClause(aObjectDataKeyPrefix + "object_data_key"_ns, + isIncreasingOrder ? ComparisonOperator::GreaterOrEquals + : ComparisonOperator::LessOrEquals, + kStmtParamNameObjectStorePosition) + + ") OR "_ns + + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterThan + : ComparisonOperator::LessThan, + kStmtParamNameCurrentKey) + + ")"_ns; + break; + + case IDBCursorDirection::Nextunique: + case IDBCursorDirection::Prevunique: + continueQuery = + aQueryStart + " AND "_ns + + GetSortKeyClause(isIncreasingOrder ? ComparisonOperator::GreaterThan + : ComparisonOperator::LessThan, + kStmtParamNameCurrentKey); + break; + + default: + MOZ_CRASH("Should never get here!"); + } + + const nsAutoCString suffix = + aDirectionClause + kOpenLimit + ":"_ns + kStmtParamNameLimit; + continueQuery += suffix; + continueToQuery += suffix; + if (!continuePrimaryKeyQuery.IsEmpty()) { + continuePrimaryKeyQuery += suffix; + } + + GetCursor().mContinueQueries.init(std::move(continueQuery), + std::move(continueToQuery), + std::move(continuePrimaryKeyQuery)); +} + +template <IDBCursorType CursorType> +nsresult CommonOpenOpHelper<CursorType>::ProcessStatementSteps( + mozIStorageStatement* const aStmt) { + QM_TRY_INSPECT(const bool& hasResult, + MOZ_TO_RESULT_INVOKE_MEMBER(aStmt, ExecuteStep)); + + if (!hasResult) { + SetResponse(void_t{}); + return NS_OK; + } + + Key previousKey; + auto* optPreviousKey = + IsUnique(GetCursor().mDirection) ? &previousKey : nullptr; + + QM_TRY_INSPECT(const auto& responseSize, + PopulateResponseFromStatement(aStmt, true, optPreviousKey)); + + // The degree to which extra responses on OpenOp can actually be used depends + // on the parameters of subsequent ContinueOp operations, see also comment in + // ContinueOp::DoDatabaseWork. + // + // TODO: We should somehow evaluate the effects of this. Maybe use a smaller + // extra count than for ContinueOp? + PopulateExtraResponses(aStmt, GetCursor().mMaxExtraCount, responseSize, + "OpenOp"_ns, optPreviousKey); + + return NS_OK; +} + +nsresult OpenOpHelper<IDBCursorType::ObjectStore>::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(GetCursor().mObjectStoreId); + + AUTO_PROFILER_LABEL("Cursor::OpenOp::DoObjectStoreDatabaseWork", DOM); + + const bool usingKeyRange = GetOptionalKeyRange().isSome(); + + const nsCString queryStart = "SELECT "_ns + kColumnNameKey + + ", file_ids, data " + "FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameId; + + const auto keyRangeClause = + DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( + GetOptionalKeyRange(), kColumnNameKey); + + const auto& directionClause = MakeDirectionClause(GetCursor().mDirection); + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + const nsCString firstQuery = queryStart + keyRangeClause + directionClause + + kOpenLimit + + IntToCString(1 + GetCursor().mMaxExtraCount); + + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement(firstQuery)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mObjectStoreId))); + + if (usingKeyRange) { + QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( + GetOptionalKeyRange().ref(), &*stmt))); + } + + // Now we need to make the query for ContinueOp. + PrepareKeyConditionClauses(directionClause, queryStart); + + return ProcessStatementSteps(&*stmt); +} + +nsresult OpenOpHelper<IDBCursorType::ObjectStoreKey>::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(GetCursor().mObjectStoreId); + + AUTO_PROFILER_LABEL("Cursor::OpenOp::DoObjectStoreKeyDatabaseWork", DOM); + + const bool usingKeyRange = GetOptionalKeyRange().isSome(); + + const nsCString queryStart = "SELECT "_ns + kColumnNameKey + + " FROM object_data " + "WHERE object_store_id = :"_ns + + kStmtParamNameId; + + const auto keyRangeClause = + DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( + GetOptionalKeyRange(), kColumnNameKey); + + const auto& directionClause = MakeDirectionClause(GetCursor().mDirection); + + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + const nsCString firstQuery = + queryStart + keyRangeClause + directionClause + kOpenLimit + "1"_ns; + + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement(firstQuery)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mObjectStoreId))); + + if (usingKeyRange) { + QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( + GetOptionalKeyRange().ref(), &*stmt))); + } + + // Now we need to make the query to get the next match. + PrepareKeyConditionClauses(directionClause, queryStart); + + return ProcessStatementSteps(&*stmt); +} + +nsresult OpenOpHelper<IDBCursorType::Index>::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(GetCursor().mObjectStoreId); + MOZ_ASSERT(GetCursor().mIndexId); + + AUTO_PROFILER_LABEL("Cursor::OpenOp::DoIndexDatabaseWork", DOM); + + const bool usingKeyRange = GetOptionalKeyRange().isSome(); + + const auto indexTable = + GetCursor().mUniqueIndex ? "unique_index_data"_ns : "index_data"_ns; + + // The result of MakeColumnPairSelectionList is stored in a local variable, + // since inlining it into the next statement causes a crash on some Mac OS X + // builds (see https://bugzilla.mozilla.org/show_bug.cgi?id=1168606#c110). + const auto columnPairSelectionList = MakeColumnPairSelectionList( + "index_table.value"_ns, "index_table.value_locale"_ns, + kColumnNameAliasSortKey, GetCursor().IsLocaleAware()); + const nsCString sortColumnAlias = + "SELECT "_ns + columnPairSelectionList + ", "_ns; + + const nsAutoCString queryStart = sortColumnAlias + + "index_table.object_data_key, " + "object_data.file_ids, " + "object_data.data " + "FROM "_ns + + indexTable + + " 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 = :"_ns + + kStmtParamNameId; + + const auto keyRangeClause = + DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( + GetOptionalKeyRange(), kColumnNameAliasSortKey); + + nsAutoCString directionClause = " ORDER BY "_ns + kColumnNameAliasSortKey; + + switch (GetCursor().mDirection) { + case IDBCursorDirection::Next: + case IDBCursorDirection::Nextunique: + directionClause.AppendLiteral(" ASC, index_table.object_data_key ASC"); + break; + + case IDBCursorDirection::Prev: + directionClause.AppendLiteral(" DESC, index_table.object_data_key DESC"); + break; + + case IDBCursorDirection::Prevunique: + 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. + const nsCString firstQuery = queryStart + keyRangeClause + directionClause + + kOpenLimit + + IntToCString(1 + GetCursor().mMaxExtraCount); + + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement(firstQuery)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mIndexId))); + + if (usingKeyRange) { + if (GetCursor().IsLocaleAware()) { + QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( + GetOptionalKeyRange().ref(), &*stmt, GetCursor().mLocale))); + } else { + QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( + GetOptionalKeyRange().ref(), &*stmt))); + } + } + + // TODO: At least the last two statements are almost the same in all + // DoDatabaseWork variants, consider removing this duplication. + + // Now we need to make the query to get the next match. + PrepareKeyConditionClauses(directionClause, std::move(queryStart)); + + return ProcessStatementSteps(&*stmt); +} + +nsresult OpenOpHelper<IDBCursorType::IndexKey>::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(GetCursor().mObjectStoreId); + MOZ_ASSERT(GetCursor().mIndexId); + + AUTO_PROFILER_LABEL("Cursor::OpenOp::DoIndexKeyDatabaseWork", DOM); + + const bool usingKeyRange = GetOptionalKeyRange().isSome(); + + const auto table = + GetCursor().mUniqueIndex ? "unique_index_data"_ns : "index_data"_ns; + + // The result of MakeColumnPairSelectionList is stored in a local variable, + // since inlining it into the next statement causes a crash on some Mac OS X + // builds (see https://bugzilla.mozilla.org/show_bug.cgi?id=1168606#c110). + const auto columnPairSelectionList = MakeColumnPairSelectionList( + "value"_ns, "value_locale"_ns, kColumnNameAliasSortKey, + GetCursor().IsLocaleAware()); + const nsCString sortColumnAlias = + "SELECT "_ns + columnPairSelectionList + ", "_ns; + + const nsAutoCString queryStart = sortColumnAlias + + "object_data_key " + " FROM "_ns + + table + " WHERE index_id = :"_ns + + kStmtParamNameId; + + const auto keyRangeClause = + DatabaseOperationBase::MaybeGetBindingClauseForKeyRange( + GetOptionalKeyRange(), kColumnNameAliasSortKey); + + nsAutoCString directionClause = " ORDER BY "_ns + kColumnNameAliasSortKey; + + switch (GetCursor().mDirection) { + case IDBCursorDirection::Next: + case IDBCursorDirection::Nextunique: + directionClause.AppendLiteral(" ASC, object_data_key ASC"); + break; + + case IDBCursorDirection::Prev: + directionClause.AppendLiteral(" DESC, object_data_key DESC"); + break; + + case IDBCursorDirection::Prevunique: + 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. + const nsCString firstQuery = + queryStart + keyRangeClause + directionClause + kOpenLimit + "1"_ns; + + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement(firstQuery)); + + QM_TRY(MOZ_TO_RESULT( + stmt->BindInt64ByName(kStmtParamNameId, GetCursor().mIndexId))); + + if (usingKeyRange) { + if (GetCursor().IsLocaleAware()) { + QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( + GetOptionalKeyRange().ref(), &*stmt, GetCursor().mLocale))); + } else { + QM_TRY(MOZ_TO_RESULT(DatabaseOperationBase::BindKeyRangeToStatement( + GetOptionalKeyRange().ref(), &*stmt))); + } + } + + // Now we need to make the query to get the next match. + PrepareKeyConditionClauses(directionClause, std::move(queryStart)); + + return ProcessStatementSteps(&*stmt); +} + +template <IDBCursorType CursorType> +nsresult Cursor<CursorType>::OpenOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(!mCursor->mContinueQueries); + + AUTO_PROFILER_LABEL("Cursor::OpenOp::DoDatabaseWork", DOM); + + auto helper = OpenOpHelper<CursorType>{*this}; + const auto rv = helper.DoDatabaseWork(aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +template <IDBCursorType CursorType> +nsresult Cursor<CursorType>::CursorOpBase::SendSuccessResult() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mCurrentlyRunningOp == this); + MOZ_ASSERT(mResponse.type() != CursorResponse::T__None); + + if (IsActorDestroyed()) { + return NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; + } + + mCursor->SendResponseInternal(mResponse, mFiles); + +#ifdef DEBUG + mResponseSent = true; +#endif + return NS_OK; +} + +template <IDBCursorType CursorType> +nsresult Cursor<CursorType>::ContinueOp::DoDatabaseWork( + DatabaseConnection* aConnection) { + MOZ_ASSERT(aConnection); + aConnection->AssertIsOnConnectionThread(); + MOZ_ASSERT(mCursor); + MOZ_ASSERT(mCursor->mObjectStoreId); + MOZ_ASSERT(!mCursor->mContinueQueries->mContinueQuery.IsEmpty()); + MOZ_ASSERT(!mCursor->mContinueQueries->mContinueToQuery.IsEmpty()); + MOZ_ASSERT(!mCurrentPosition.mKey.IsUnset()); + + if constexpr (IsIndexCursor) { + MOZ_ASSERT_IF( + mCursor->mDirection == IDBCursorDirection::Next || + mCursor->mDirection == IDBCursorDirection::Prev, + !mCursor->mContinueQueries->mContinuePrimaryKeyQuery.IsEmpty()); + MOZ_ASSERT(mCursor->mIndexId); + MOZ_ASSERT(!mCurrentPosition.mObjectStoreKey.IsUnset()); + } + + AUTO_PROFILER_LABEL("Cursor::ContinueOp::DoDatabaseWork", DOM); + + // 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 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. + // + // TODO: The description above is not complete, it does not take account of + // ContinuePrimaryKey nor Advance. + // + // Note: Changing the number or order of SELECT columns in the query will + // require changes to CursorOpBase::PopulateResponseFromStatement. + + const uint32_t advanceCount = + mParams.type() == CursorRequestParams::TAdvanceParams + ? mParams.get_AdvanceParams().count() + : 1; + MOZ_ASSERT(advanceCount > 0); + + bool hasContinueKey = false; + bool hasContinuePrimaryKey = false; + + auto explicitContinueKey = Key{}; + + switch (mParams.type()) { + case CursorRequestParams::TContinueParams: + if (!mParams.get_ContinueParams().key().IsUnset()) { + hasContinueKey = true; + explicitContinueKey = 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 == IDBCursorDirection::Next || + mCursor->mDirection == IDBCursorDirection::Prev); + hasContinueKey = true; + hasContinuePrimaryKey = true; + explicitContinueKey = mParams.get_ContinuePrimaryKeyParams().key(); + break; + case CursorRequestParams::TAdvanceParams: + break; + default: + MOZ_CRASH("Should never get here!"); + } + + // TODO: Whether it makes sense to preload depends on the kind of the + // subsequent operations, not of the current operation. We could assume that + // the subsequent operations are: + // - the same as the current operation (with the same parameter values) + // - as above, except for Advance, where we assume the count will be 1 on the + // next call + // - basic operations (Advance with count 1 or Continue-without-key) + // + // For now, we implement the second option for now (which correspond to + // !hasContinueKey). + // + // Based on that, we could in both cases either preload for any assumed + // subsequent operations, or only for the basic operations. For now, we + // preload only for an assumed basic operation. Other operations would require + // more work on the client side for invalidation, and may not make any sense + // at all. + const uint32_t maxExtraCount = hasContinueKey ? 0 : mCursor->mMaxExtraCount; + + QM_TRY_INSPECT(const auto& stmt, + aConnection->BorrowCachedStatement( + mCursor->mContinueQueries->GetContinueQuery( + hasContinueKey, hasContinuePrimaryKey))); + + QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName( + kStmtParamNameLimit, + IntToCString(advanceCount + mCursor->mMaxExtraCount)))); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName(kStmtParamNameId, mCursor->Id()))); + + // Bind current key. + const auto& continueKey = + hasContinueKey ? explicitContinueKey + : mCurrentPosition.GetSortKey(mCursor->IsLocaleAware()); + QM_TRY(MOZ_TO_RESULT( + continueKey.BindToStatement(&*stmt, kStmtParamNameCurrentKey))); + + // Bind range bound if it is specified. + if (!mCursor->mLocaleAwareRangeBound->IsUnset()) { + QM_TRY(MOZ_TO_RESULT(mCursor->mLocaleAwareRangeBound->BindToStatement( + &*stmt, kStmtParamNameRangeBound))); + } + + // Bind object store position if duplicates are allowed and we're not + // continuing to a specific key. + if constexpr (IsIndexCursor) { + if (!hasContinueKey && (mCursor->mDirection == IDBCursorDirection::Next || + mCursor->mDirection == IDBCursorDirection::Prev)) { + QM_TRY(MOZ_TO_RESULT(mCurrentPosition.mObjectStoreKey.BindToStatement( + &*stmt, kStmtParamNameObjectStorePosition))); + } else if (hasContinuePrimaryKey) { + QM_TRY(MOZ_TO_RESULT( + mParams.get_ContinuePrimaryKeyParams().primaryKey().BindToStatement( + &*stmt, kStmtParamNameObjectStorePosition))); + } + } + + // TODO: Why do we query the records we don't need and skip them here, rather + // than using a OFFSET clause in the query? + for (uint32_t index = 0; index < advanceCount; index++) { + QM_TRY_INSPECT(const bool& hasResult, + MOZ_TO_RESULT_INVOKE_MEMBER(&*stmt, ExecuteStep)); + + if (!hasResult) { + mResponse = void_t(); + return NS_OK; + } + } + + Key previousKey; + auto* const optPreviousKey = + IsUnique(mCursor->mDirection) ? &previousKey : nullptr; + + auto helper = CursorOpBaseHelperBase<CursorType>{*this}; + QM_TRY_INSPECT(const auto& responseSize, helper.PopulateResponseFromStatement( + &*stmt, true, optPreviousKey)); + + helper.PopulateExtraResponses(&*stmt, maxExtraCount, responseSize, + "ContinueOp"_ns, optPreviousKey); + + 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 +} + +mozilla::ipc::IPCResult Utils::RecvDeleteMe() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + QM_WARNONLY_TRY(OkIf(PBackgroundIndexedDBUtilsParent::Send__delete__(this))); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Utils::RecvGetFileReferences( + const PersistenceType& aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseName, const int64_t& aFileId, int32_t* aRefCnt, + int32_t* aDBRefCnt, bool* aResult) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aRefCnt); + MOZ_ASSERT(aDBRefCnt); + MOZ_ASSERT(aResult); + MOZ_ASSERT(!mActorDestroyed); + + if (NS_WARN_IF(!IndexedDatabaseManager::Get())) { + return IPC_FAIL(this, "No IndexedDatabaseManager active!"); + } + + if (NS_WARN_IF(!QuotaManager::Get())) { + return IPC_FAIL(this, "No QuotaManager active!"); + } + + if (NS_WARN_IF(!StaticPrefs::dom_indexedDB_testing())) { + return IPC_FAIL(this, "IndexedDB is not in testing mode!"); + } + + if (NS_WARN_IF(!IsValidPersistenceType(aPersistenceType))) { + return IPC_FAIL(this, "PersistenceType is not valid!"); + } + + if (NS_WARN_IF(aOrigin.IsEmpty())) { + return IPC_FAIL(this, "Origin is empty!"); + } + + if (NS_WARN_IF(aDatabaseName.IsEmpty())) { + return IPC_FAIL(this, "DatabaseName is empty!"); + } + + if (NS_WARN_IF(aFileId == 0)) { + return IPC_FAIL(this, "No FileId!"); + } + + nsresult rv = + DispatchAndReturnFileReferences(aPersistenceType, aOrigin, aDatabaseName, + aFileId, aRefCnt, aDBRefCnt, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_FAIL(this, "DispatchAndReturnFileReferences failed!"); + } + + return IPC_OK(); +} + +#ifdef DEBUG + +NS_IMPL_ISUPPORTS(DEBUGThreadSlower, nsIThreadObserver) + +NS_IMETHODIMP +DEBUGThreadSlower::OnDispatchedEvent() { 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()); + + auto fileDirectory = mFileManager->GetCheckedDirectory(); + if (NS_WARN_IF(!fileDirectory)) { + return NS_ERROR_FAILURE; + } + + auto 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.init(WrapNotNullUnchecked(std::move(fileDirectory))); + mJournalDirectory.init(WrapNotNullUnchecked(std::move(journalDirectory))); + + return NS_OK; +} + +nsCOMPtr<nsIFile> FileHelper::GetFile(const DatabaseFileInfo& aFileInfo) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + return mFileManager->GetFileForId(mFileDirectory->get(), aFileInfo.Id()); +} + +nsCOMPtr<nsIFile> FileHelper::GetJournalFile( + const DatabaseFileInfo& aFileInfo) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + return mFileManager->GetFileForId(mJournalDirectory->get(), aFileInfo.Id()); +} + +nsresult FileHelper::CreateFileFromStream(nsIFile& aFile, nsIFile& aJournalFile, + nsIInputStream& aInputStream, + bool aCompress, + const Maybe<CipherKey>& aMaybeKey) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + QM_TRY_INSPECT(const auto& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists)); + + // 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) { + QM_TRY_INSPECT(const auto& isFile, + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsFile)); + + QM_TRY(OkIf(isFile), NS_ERROR_FAILURE); + + QM_TRY_INSPECT(const auto& journalExists, + MOZ_TO_RESULT_INVOKE_MEMBER(aJournalFile, Exists)); + + QM_TRY(OkIf(journalExists), NS_ERROR_FAILURE); + + QM_TRY_INSPECT(const auto& journalIsFile, + MOZ_TO_RESULT_INVOKE_MEMBER(aJournalFile, IsFile)); + + QM_TRY(OkIf(journalIsFile), NS_ERROR_FAILURE); + + IDB_WARNING("Deleting orphaned file!"); + + QM_TRY(MOZ_TO_RESULT(mFileManager->SyncDeleteFile(aFile, aJournalFile))); + } + + // Create a journal file first. + QM_TRY(MOZ_TO_RESULT(aJournalFile.Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + + // Now try to copy the stream. + QM_TRY_UNWRAP(nsCOMPtr<nsIOutputStream> fileOutputStream, + CreateFileOutputStream(mFileManager->Type(), + mFileManager->OriginMetadata(), + Client::IDB, &aFile)); + + AutoTArray<char, kFileCopyBufferSize> buffer; + const auto actualOutputStream = + [aCompress, &aMaybeKey, &buffer, + baseOutputStream = + std::move(fileOutputStream)]() mutable -> nsCOMPtr<nsIOutputStream> { + if (aMaybeKey) { + baseOutputStream = + MakeRefPtr<EncryptingOutputStream<IndexedDBCipherStrategy>>( + std::move(baseOutputStream), kEncryptedStreamBlockSize, + *aMaybeKey); + } + + if (aCompress) { + auto snappyOutputStream = + MakeRefPtr<SnappyCompressOutputStream>(baseOutputStream); + + buffer.SetLength(snappyOutputStream->BlockSize()); + + return snappyOutputStream; + } + + buffer.SetLength(kFileCopyBufferSize); + return std::move(baseOutputStream); + }(); + + QM_TRY(MOZ_TO_RESULT(SyncCopy(aInputStream, *actualOutputStream, + buffer.Elements(), buffer.Length()))); + + return NS_OK; +} + +class FileHelper::ReadCallback final : public nsIInputStreamCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + ReadCallback() + : mMutex("ReadCallback::mMutex"), + mCondVar(mMutex, "ReadCallback::mCondVar"), + mInputAvailable(false) {} + + NS_IMETHOD + OnInputStreamReady(nsIAsyncInputStream* aStream) override { + mozilla::MutexAutoLock autolock(mMutex); + + mInputAvailable = true; + mCondVar.Notify(); + + return NS_OK; + } + + nsresult AsyncWait(nsIAsyncInputStream* aStream, uint32_t aBufferSize, + nsIEventTarget* aTarget) { + MOZ_ASSERT(aStream); + mozilla::MutexAutoLock autolock(mMutex); + + nsresult rv = aStream->AsyncWait(this, 0, aBufferSize, aTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInputAvailable = false; + while (!mInputAvailable) { + mCondVar.Wait(); + } + + return NS_OK; + } + + private: + ~ReadCallback() = default; + + mozilla::Mutex mMutex MOZ_UNANNOTATED; + mozilla::CondVar mCondVar; + bool mInputAvailable; +}; + +NS_IMPL_ADDREF(FileHelper::ReadCallback); +NS_IMPL_RELEASE(FileHelper::ReadCallback); + +NS_INTERFACE_MAP_BEGIN(FileHelper::ReadCallback) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback) +NS_INTERFACE_MAP_END + +nsresult FileHelper::SyncRead(nsIInputStream& aInputStream, char* const aBuffer, + const uint32_t aBufferSize, + uint32_t* const aRead) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + // Let's try to read, directly. + nsresult rv = aInputStream.Read(aBuffer, aBufferSize, aRead); + if (NS_SUCCEEDED(rv) || rv != NS_BASE_STREAM_WOULD_BLOCK) { + return rv; + } + + // We need to proceed async. + nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(&aInputStream); + if (!asyncStream) { + return rv; + } + + if (!mReadCallback) { + mReadCallback.init(MakeNotNull<RefPtr<ReadCallback>>()); + } + + // We just need any thread with an event loop for receiving the + // OnInputStreamReady callback. Let's use the I/O thread. + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + rv = (*mReadCallback)->AsyncWait(asyncStream, aBufferSize, target); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return SyncRead(aInputStream, aBuffer, aBufferSize, aRead); +} + +nsresult FileHelper::SyncCopy(nsIInputStream& aInputStream, + nsIOutputStream& aOutputStream, + char* const aBuffer, const uint32_t aBufferSize) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + AUTO_PROFILER_LABEL("FileHelper::SyncCopy", DOM); + + nsresult rv; + + do { + uint32_t numRead; + rv = SyncRead(aInputStream, 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 dom::indexedDB +} // namespace mozilla + +#undef IDB_MOBILE +#undef IDB_DEBUG_LOG diff --git a/dom/indexedDB/ActorsParent.h b/dom/indexedDB/ActorsParent.h new file mode 100644 index 0000000000..ab6c6c142f --- /dev/null +++ b/dom/indexedDB/ActorsParent.h @@ -0,0 +1,62 @@ +/* -*- 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__ + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/PBrowserParent.h" +#include "mozilla/RefPtr.h" +#include "nsIPermissionManager.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class Element; +class FileHandleThreadPool; + +namespace quota { + +class Client; + +} // namespace quota + +namespace indexedDB { + +enum class PermissionValue { + kPermissionAllowed = nsIPermissionManager::ALLOW_ACTION, + kPermissionDenied = nsIPermissionManager::DENY_ACTION, + kPermissionPrompt = nsIPermissionManager::PROMPT_ACTION +}; + +class LoggingInfo; +class PBackgroundIDBFactoryParent; +class PBackgroundIndexedDBUtilsParent; + +already_AddRefed<PBackgroundIDBFactoryParent> AllocPBackgroundIDBFactoryParent( + const LoggingInfo& aLoggingInfo); + +bool RecvPBackgroundIDBFactoryConstructor(PBackgroundIDBFactoryParent* aActor, + const LoggingInfo& aLoggingInfo); + +bool DeallocPBackgroundIDBFactoryParent(PBackgroundIDBFactoryParent* aActor); + +PBackgroundIndexedDBUtilsParent* AllocPBackgroundIndexedDBUtilsParent(); + +bool DeallocPBackgroundIndexedDBUtilsParent( + PBackgroundIndexedDBUtilsParent* aActor); + +bool RecvFlushPendingFileDeletions(); + +RefPtr<mozilla::dom::quota::Client> CreateQuotaClient(); + +FileHandleThreadPool* GetFileHandleThreadPool(); + +} // namespace indexedDB +} // namespace mozilla::dom + +#endif // mozilla_dom_indexeddb_actorsparent_h__ diff --git a/dom/indexedDB/ActorsParentCommon.cpp b/dom/indexedDB/ActorsParentCommon.cpp new file mode 100644 index 0000000000..52e35c66f5 --- /dev/null +++ b/dom/indexedDB/ActorsParentCommon.cpp @@ -0,0 +1,739 @@ +/* -*- 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 "ActorsParentCommon.h" + +// local includes +#include "DatabaseFileInfo.h" +#include "DatabaseFileManager.h" +#include "IndexedDatabase.h" // for StructuredCloneFile... +#include "IndexedDatabaseInlines.h" +#include "IndexedDatabaseManager.h" +#include "IndexedDBCipherKeyManager.h" +#include "IndexedDBCommon.h" +#include "ReportInternalError.h" + +// global includes +#include <stdlib.h> +#include <string.h> +#include <algorithm> +#include <numeric> +#include <type_traits> +#include "MainThreadUtils.h" +#include "SafeRefPtr.h" +#include "js/RootingAPI.h" +#include "js/StructuredClone.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/JSObjectHolder.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryScalarEnums.h" +#include "mozilla/dom/quota/DecryptingInputStream_impl.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" +#include "mozilla/fallible.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/mozalloc.h" +#include "nsCOMPtr.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIInputStream.h" +#include "nsIPrincipal.h" +#include "nsIXPConnect.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsXULAppAPI.h" +#include "snappy/snappy.h" + +class nsIFile; + +namespace mozilla::dom::indexedDB { + +static_assert(SNAPPY_VERSION == 0x010109); + +using mozilla::ipc::IsOnBackgroundThread; + +const nsLiteralString kJournalDirectoryName = u"journals"_ns; + +namespace { + +constexpr StructuredCloneFileBase::FileType ToStructuredCloneFileType( + const char16_t aTag) { + switch (aTag) { + case char16_t('-'): + return StructuredCloneFileBase::eMutableFile; + + case char16_t('.'): + return StructuredCloneFileBase::eStructuredClone; + + case char16_t('/'): + return StructuredCloneFileBase::eWasmBytecode; + + case char16_t('\\'): + return StructuredCloneFileBase::eWasmCompiled; + + default: + return StructuredCloneFileBase::eBlob; + } +} + +int32_t ToInteger(const nsAString& aStr, nsresult* const aRv) { + return aStr.ToInteger(aRv); +} + +Result<StructuredCloneFileParent, nsresult> DeserializeStructuredCloneFile( + const DatabaseFileManager& aFileManager, + const nsDependentSubstring& aText) { + MOZ_ASSERT(!aText.IsEmpty()); + + const StructuredCloneFileBase::FileType type = + ToStructuredCloneFileType(aText.First()); + + QM_TRY_INSPECT(const auto& id, + MOZ_TO_RESULT_GET_TYPED( + int32_t, ToInteger, + type == StructuredCloneFileBase::eBlob + ? aText + : static_cast<const nsAString&>(Substring(aText, 1)))); + + SafeRefPtr<DatabaseFileInfo> fileInfo = aFileManager.GetFileInfo(id); + MOZ_ASSERT(fileInfo); + // XXX In bug 1432133, for some reasons DatabaseFileInfo object cannot be + // got. This is just a short-term fix, and we are working on finding the real + // cause in bug 1519859. + QM_TRY(OkIf((bool)fileInfo), Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + + return StructuredCloneFileParent{type, std::move(fileInfo)}; +} + +// This class helps to create only 1 sandbox. +class SandboxHolder final { + public: + NS_INLINE_DECL_REFCOUNTING(SandboxHolder) + + private: + friend JSObject* mozilla::dom::indexedDB::GetSandbox(JSContext* aCx); + + ~SandboxHolder() = default; + + static SandboxHolder* GetOrCreate() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + static StaticRefPtr<SandboxHolder> sHolder; + if (!sHolder) { + sHolder = new SandboxHolder(); + ClearOnShutdown(&sHolder); + } + return sHolder; + } + + JSObject* GetSandboxInternal(JSContext* aCx) { + if (!mSandbox) { + nsIXPConnect* const xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + + // Let's use a null principal. + const nsCOMPtr<nsIPrincipal> principal = + NullPrincipal::CreateWithoutOriginAttributes(); + + JS::Rooted<JSObject*> sandbox(aCx); + QM_TRY( + MOZ_TO_RESULT(xpc->CreateSandbox(aCx, principal, sandbox.address())), + nullptr); + + mSandbox = new JSObjectHolder(aCx, sandbox); + } + + return mSandbox->GetJSObject(); + } + + RefPtr<JSObjectHolder> mSandbox; +}; + +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(IndexOrObjectStoreId 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* const 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)); +} + +void WriteCompressedIndexId(IndexOrObjectStoreId 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); +} + +// aOutIndexValues is an output parameter, since its storage is reused. +nsresult ReadCompressedIndexDataValuesFromBlob( + const Span<const uint8_t> aBlobData, + nsTArray<IndexDataValue>* aOutIndexValues) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(!aBlobData.IsEmpty()); + MOZ_ASSERT(aOutIndexValues); + MOZ_ASSERT(aOutIndexValues->IsEmpty()); + + AUTO_PROFILER_LABEL("ReadCompressedIndexDataValuesFromBlob", DOM); + + // XXX Is this check still necessary with a Span? Or should it rather be moved + // to the caller? + QM_TRY(OkIf(uintptr_t(aBlobData.Elements()) <= + UINTPTR_MAX - aBlobData.LengthBytes()), + NS_ERROR_FILE_CORRUPTED, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + for (auto remainder = aBlobData; !remainder.IsEmpty();) { + QM_TRY_INSPECT((const auto& [indexId, unique, remainderAfterIndexId]), + ReadCompressedIndexId(remainder)); + + QM_TRY(OkIf(!remainderAfterIndexId.IsEmpty()), NS_ERROR_FILE_CORRUPTED, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + // Read key buffer length. + QM_TRY_INSPECT( + (const auto& [keyBufferLength, remainderAfterKeyBufferLength]), + ReadCompressedNumber(remainderAfterIndexId)); + + QM_TRY(OkIf(!remainderAfterKeyBufferLength.IsEmpty()), + NS_ERROR_FILE_CORRUPTED, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + QM_TRY(OkIf(keyBufferLength <= uint64_t(UINT32_MAX)), + NS_ERROR_FILE_CORRUPTED, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + QM_TRY(OkIf(keyBufferLength <= remainderAfterKeyBufferLength.Length()), + NS_ERROR_FILE_CORRUPTED, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + const auto [keyBuffer, remainderAfterKeyBuffer] = + remainderAfterKeyBufferLength.SplitAt(keyBufferLength); + auto idv = + IndexDataValue{indexId, unique, Key{nsCString{AsChars(keyBuffer)}}}; + + // Read sort key buffer length. + QM_TRY_INSPECT( + (const auto& [sortKeyBufferLength, remainderAfterSortKeyBufferLength]), + ReadCompressedNumber(remainderAfterKeyBuffer)); + + remainder = remainderAfterSortKeyBufferLength; + if (sortKeyBufferLength > 0) { + QM_TRY(OkIf(!remainder.IsEmpty()), NS_ERROR_FILE_CORRUPTED, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + QM_TRY(OkIf(sortKeyBufferLength <= uint64_t(UINT32_MAX)), + NS_ERROR_FILE_CORRUPTED, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + QM_TRY(OkIf(sortKeyBufferLength <= remainder.Length()), + NS_ERROR_FILE_CORRUPTED, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + const auto [sortKeyBuffer, remainderAfterSortKeyBuffer] = + remainder.SplitAt(sortKeyBufferLength); + idv.mLocaleAwarePosition = Key{nsCString{AsChars(sortKeyBuffer)}}; + remainder = remainderAfterSortKeyBuffer; + } + + QM_TRY(OkIf(aOutIndexValues->AppendElement(std::move(idv), fallible)), + NS_ERROR_OUT_OF_MEMORY, IDB_REPORT_INTERNAL_ERR_LAMBDA); + } + aOutIndexValues->Sort(); + + return NS_OK; +} + +// aOutIndexValues is an output parameter, since its storage is reused. +template <typename T> +nsresult ReadCompressedIndexDataValuesFromSource( + T& aSource, uint32_t aColumnIndex, + nsTArray<IndexDataValue>* aOutIndexValues) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aOutIndexValues); + MOZ_ASSERT(aOutIndexValues->IsEmpty()); + + QM_TRY_INSPECT( + const int32_t& columnType, + MOZ_TO_RESULT_INVOKE_MEMBER(aSource, GetTypeOfIndex, aColumnIndex)); + + switch (columnType) { + case mozIStorageStatement::VALUE_TYPE_NULL: + return NS_OK; + + case mozIStorageStatement::VALUE_TYPE_BLOB: { + // XXX ToResultInvoke does not support multiple output parameters yet, so + // we also can't use QM_TRY_UNWRAP/QM_TRY_INSPECT here. + const uint8_t* blobData; + uint32_t blobDataLength; + QM_TRY(MOZ_TO_RESULT( + aSource.GetSharedBlob(aColumnIndex, &blobDataLength, &blobData))); + + QM_TRY(OkIf(blobDataLength), NS_ERROR_FILE_CORRUPTED, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + QM_TRY(MOZ_TO_RESULT(ReadCompressedIndexDataValuesFromBlob( + Span(blobData, blobDataLength), aOutIndexValues))); + + return NS_OK; + } + + default: + return NS_ERROR_FILE_CORRUPTED; + } +} + +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromBlob(const uint8_t* aBlobData, + uint32_t aBlobDataLength, + const DatabaseFileManager& aFileManager, + const nsAString& aFileIds) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + AUTO_PROFILER_LABEL("GetStructuredCloneReadInfoFromBlob", DOM); + + const char* const compressed = reinterpret_cast<const char*>(aBlobData); + const size_t compressedLength = size_t(aBlobDataLength); + + size_t uncompressedLength; + QM_TRY(OkIf(snappy::GetUncompressedLength(compressed, compressedLength, + &uncompressedLength)), + Err(NS_ERROR_FILE_CORRUPTED)); + + // `data` (JSStructuredCloneData) currently uses 4k buffer internally. + // For performance reasons, it's better to align `uncompressed` with that. + AutoTArray<uint8_t, 4096> uncompressed; + QM_TRY(OkIf(uncompressed.SetLength(uncompressedLength, fallible)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + char* const uncompressedBuffer = + reinterpret_cast<char*>(uncompressed.Elements()); + + QM_TRY(OkIf(snappy::RawUncompress(compressed, compressedLength, + uncompressedBuffer)), + Err(NS_ERROR_FILE_CORRUPTED)); + + JSStructuredCloneData data(JS::StructuredCloneScope::DifferentProcess); + QM_TRY(OkIf(data.AppendBytes(uncompressedBuffer, uncompressed.Length())), + Err(NS_ERROR_OUT_OF_MEMORY)); + + nsTArray<StructuredCloneFileParent> files; + if (!aFileIds.IsVoid()) { + QM_TRY_UNWRAP(files, + DeserializeStructuredCloneFiles(aFileManager, aFileIds)); + } + + return StructuredCloneReadInfoParent{std::move(data), std::move(files), + false}; +} + +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromExternalBlob( + uint64_t aIntData, const DatabaseFileManager& aFileManager, + const nsAString& aFileIds) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + AUTO_PROFILER_LABEL("GetStructuredCloneReadInfoFromExternalBlob", DOM); + + nsTArray<StructuredCloneFileParent> files; + if (!aFileIds.IsVoid()) { + QM_TRY_UNWRAP(files, + DeserializeStructuredCloneFiles(aFileManager, aFileIds)); + } + + // Higher and lower 32 bits described + // in ObjectStoreAddOrPutRequestOp::DoDatabaseWork. + const uint32_t index = uint32_t(aIntData & UINT32_MAX); + + QM_TRY(OkIf(index < files.Length()), Err(NS_ERROR_UNEXPECTED), + [](const auto&) { MOZ_ASSERT(false, "Bad index value!"); }); + + if (StaticPrefs::dom_indexedDB_preprocessing()) { + return StructuredCloneReadInfoParent{ + JSStructuredCloneData{JS::StructuredCloneScope::DifferentProcess}, + std::move(files), true}; + } + + // XXX Why can there be multiple files, but we use only a single one here? + const StructuredCloneFileParent& file = files[index]; + MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eStructuredClone); + + Maybe<CipherKey> maybeKey; + + if (aFileManager.IsInPrivateBrowsingMode()) { + nsCString fileKeyId; + fileKeyId.AppendInt(file.FileInfo().Id()); + + maybeKey = aFileManager.MutableCipherKeyManagerRef().Get(fileKeyId); + } + + auto data = JSStructuredCloneData{JS::StructuredCloneScope::DifferentProcess}; + + { + const nsCOMPtr<nsIFile> nativeFile = file.FileInfo().GetFileForFileInfo(); + QM_TRY(OkIf(nativeFile), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT( + const auto& fileInputStream, + NS_NewLocalFileInputStream(nativeFile) + .andThen([maybeKey](auto fileInputStream) + -> Result<nsCOMPtr<nsIInputStream>, nsresult> { + if (maybeKey) { + return nsCOMPtr<nsIInputStream>{MakeRefPtr< + quota::DecryptingInputStream<IndexedDBCipherStrategy>>( + WrapNotNull(std::move(fileInputStream)), + kEncryptedStreamBlockSize, *maybeKey)}; + } + + return fileInputStream; + })); + + QM_TRY(MOZ_TO_RESULT( + SnappyUncompressStructuredCloneData(*fileInputStream, data))); + } + + return StructuredCloneReadInfoParent{std::move(data), std::move(files), + false}; +} + +template <typename T> +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromSource(T* aSource, uint32_t aDataIndex, + uint32_t aFileIdsIndex, + const DatabaseFileManager& aFileManager) { + MOZ_ASSERT(!IsOnBackgroundThread()); + MOZ_ASSERT(aSource); + + QM_TRY_INSPECT( + const int32_t& columnType, + MOZ_TO_RESULT_INVOKE_MEMBER(aSource, GetTypeOfIndex, aDataIndex)); + + QM_TRY_INSPECT(const bool& isNull, MOZ_TO_RESULT_INVOKE_MEMBER( + aSource, GetIsNull, aFileIdsIndex)); + + QM_TRY_INSPECT(const nsString& fileIds, ([aSource, aFileIdsIndex, isNull] { + return isNull ? Result<nsString, nsresult>{VoidString()} + : MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, aSource, GetString, + aFileIdsIndex); + }())); + + switch (columnType) { + case mozIStorageStatement::VALUE_TYPE_INTEGER: { + QM_TRY_INSPECT( + const int64_t& intData, + MOZ_TO_RESULT_INVOKE_MEMBER(aSource, GetInt64, aDataIndex)); + + uint64_t uintData; + memcpy(&uintData, &intData, sizeof(uint64_t)); + + return GetStructuredCloneReadInfoFromExternalBlob(uintData, aFileManager, + fileIds); + } + + case mozIStorageStatement::VALUE_TYPE_BLOB: { + const uint8_t* blobData; + uint32_t blobDataLength; + QM_TRY(MOZ_TO_RESULT( + aSource->GetSharedBlob(aDataIndex, &blobDataLength, &blobData))); + + return GetStructuredCloneReadInfoFromBlob(blobData, blobDataLength, + aFileManager, fileIds); + } + + default: + return Err(NS_ERROR_FILE_CORRUPTED); + } +} + +} // namespace + +IndexDataValue::IndexDataValue() : mIndexId(0), mUnique(false) { + MOZ_COUNT_CTOR(IndexDataValue); +} + +#ifdef NS_BUILD_REFCNT_LOGGING +IndexDataValue::IndexDataValue(IndexDataValue&& aOther) + : mIndexId(aOther.mIndexId), + mPosition(std::move(aOther.mPosition)), + mLocaleAwarePosition(std::move(aOther.mLocaleAwarePosition)), + mUnique(aOther.mUnique) { + MOZ_ASSERT(!aOther.mPosition.IsUnset()); + + MOZ_COUNT_CTOR(IndexDataValue); +} +#endif + +IndexDataValue::IndexDataValue(IndexOrObjectStoreId aIndexId, bool aUnique, + const Key& aPosition) + : mIndexId(aIndexId), mPosition(aPosition), mUnique(aUnique) { + MOZ_ASSERT(!aPosition.IsUnset()); + + MOZ_COUNT_CTOR(IndexDataValue); +} + +IndexDataValue::IndexDataValue(IndexOrObjectStoreId aIndexId, bool aUnique, + const Key& aPosition, + const Key& aLocaleAwarePosition) + : mIndexId(aIndexId), + mPosition(aPosition), + mLocaleAwarePosition(aLocaleAwarePosition), + mUnique(aUnique) { + MOZ_ASSERT(!aPosition.IsUnset()); + + MOZ_COUNT_CTOR(IndexDataValue); +} + +bool IndexDataValue::operator==(const IndexDataValue& aOther) const { + if (mIndexId != aOther.mIndexId) { + return false; + } + if (mLocaleAwarePosition.IsUnset()) { + return mPosition == aOther.mPosition; + } + return mLocaleAwarePosition == aOther.mLocaleAwarePosition; +} + +bool IndexDataValue::operator<(const IndexDataValue& aOther) const { + if (mIndexId == aOther.mIndexId) { + if (mLocaleAwarePosition.IsUnset()) { + return mPosition < aOther.mPosition; + } + return mLocaleAwarePosition < aOther.mLocaleAwarePosition; + } + + return mIndexId < aOther.mIndexId; +} + +JSObject* GetSandbox(JSContext* aCx) { + SandboxHolder* holder = SandboxHolder::GetOrCreate(); + return holder->GetSandboxInternal(aCx); +} + +Result<std::pair<UniqueFreePtr<uint8_t>, uint32_t>, nsresult> +MakeCompressedIndexDataValues(const nsTArray<IndexDataValue>& aIndexValues) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + AUTO_PROFILER_LABEL("MakeCompressedIndexDataValues", DOM); + + const uint32_t arrayLength = aIndexValues.Length(); + if (!arrayLength) { + return std::pair{UniqueFreePtr<uint8_t>{}, 0u}; + } + + // First calculate the size of the final buffer. + const auto blobDataLength = std::accumulate( + aIndexValues.cbegin(), aIndexValues.cend(), CheckedUint32(0), + [](CheckedUint32 sum, const IndexDataValue& info) { + const nsCString& keyBuffer = info.mPosition.GetBuffer(); + const nsCString& sortKeyBuffer = info.mLocaleAwarePosition.GetBuffer(); + const uint32_t keyBufferLength = keyBuffer.Length(); + const uint32_t sortKeyBufferLength = sortKeyBuffer.Length(); + + MOZ_ASSERT(!keyBuffer.IsEmpty()); + + return sum + CompressedByteCountForIndexId(info.mIndexId) + + CompressedByteCountForNumber(keyBufferLength) + + CompressedByteCountForNumber(sortKeyBufferLength) + + keyBufferLength + sortKeyBufferLength; + }); + + QM_TRY(OkIf(blobDataLength.isValid()), + Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + UniqueFreePtr<uint8_t> blobData( + static_cast<uint8_t*>(malloc(blobDataLength.value()))); + QM_TRY(OkIf(static_cast<bool>(blobData)), Err(NS_ERROR_OUT_OF_MEMORY), + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + uint8_t* blobDataIter = blobData.get(); + + for (const IndexDataValue& info : aIndexValues) { + const nsCString& keyBuffer = info.mPosition.GetBuffer(); + const nsCString& sortKeyBuffer = info.mLocaleAwarePosition.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.value()); + + return std::pair{std::move(blobData), blobDataLength.value()}; +} + +nsresult ReadCompressedIndexDataValues( + mozIStorageStatement& aStatement, uint32_t aColumnIndex, + nsTArray<IndexDataValue>& aOutIndexValues) { + return ReadCompressedIndexDataValuesFromSource(aStatement, aColumnIndex, + &aOutIndexValues); +} + +template <typename T> +Result<IndexDataValuesAutoArray, nsresult> ReadCompressedIndexDataValues( + T& aValues, uint32_t aColumnIndex) { + return MOZ_TO_RESULT_INVOKE_TYPED(IndexDataValuesAutoArray, + &ReadCompressedIndexDataValuesFromSource<T>, + aValues, aColumnIndex); +} + +template Result<IndexDataValuesAutoArray, nsresult> +ReadCompressedIndexDataValues<mozIStorageValueArray>(mozIStorageValueArray&, + uint32_t); + +template Result<IndexDataValuesAutoArray, nsresult> +ReadCompressedIndexDataValues<mozIStorageStatement>(mozIStorageStatement&, + uint32_t); + +Result<std::tuple<IndexOrObjectStoreId, bool, Span<const uint8_t>>, nsresult> +ReadCompressedIndexId(const Span<const uint8_t> aData) { + QM_TRY_INSPECT((const auto& [indexId, remainder]), + ReadCompressedNumber(aData)); + + MOZ_ASSERT(UINT64_MAX / 2 >= uint64_t(indexId), "Bad index id!"); + + return std::tuple{IndexOrObjectStoreId(indexId >> 1), indexId % 2 == 1, + remainder}; +} + +Result<std::pair<uint64_t, mozilla::Span<const uint8_t>>, nsresult> +ReadCompressedNumber(const Span<const uint8_t> aSpan) { + uint8_t shiftCounter = 0; + uint64_t result = 0; + + const auto end = aSpan.cend(); + + const auto newPos = + std::find_if(aSpan.cbegin(), end, [&result, &shiftCounter](uint8_t byte) { + MOZ_ASSERT(shiftCounter <= 56, "Shifted too many bits!"); + + result += (uint64_t(byte & 0x7f) << shiftCounter); + shiftCounter += 7; + + return !(byte & 0x80); + }); + + QM_TRY(OkIf(newPos != end), Err(NS_ERROR_FILE_CORRUPTED), [](const auto&) { + MOZ_ASSERT(false); + IDB_REPORT_INTERNAL_ERR(); + }); + + return std::pair{result, Span{newPos + 1, end}}; +} + +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromValueArray( + mozIStorageValueArray* aValues, uint32_t aDataIndex, uint32_t aFileIdsIndex, + const DatabaseFileManager& aFileManager) { + return GetStructuredCloneReadInfoFromSource(aValues, aDataIndex, + aFileIdsIndex, aFileManager); +} + +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromStatement( + mozIStorageStatement* aStatement, uint32_t aDataIndex, + uint32_t aFileIdsIndex, const DatabaseFileManager& aFileManager) { + return GetStructuredCloneReadInfoFromSource(aStatement, aDataIndex, + aFileIdsIndex, aFileManager); +} + +Result<nsTArray<StructuredCloneFileParent>, nsresult> +DeserializeStructuredCloneFiles(const DatabaseFileManager& aFileManager, + const nsAString& aText) { + MOZ_ASSERT(!IsOnBackgroundThread()); + + nsTArray<StructuredCloneFileParent> result; + for (const auto& token : + nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>(aText, ' ') + .ToRange()) { + MOZ_ASSERT(!token.IsEmpty()); + + QM_TRY_UNWRAP(auto structuredCloneFile, + DeserializeStructuredCloneFile(aFileManager, token)); + + result.EmplaceBack(std::move(structuredCloneFile)); + } + + return result; +} + +nsresult ExecuteSimpleSQLSequence(mozIStorageConnection& aConnection, + Span<const nsLiteralCString> aSQLCommands) { + for (const auto& aSQLCommand : aSQLCommands) { + const auto extraInfo = quota::ScopedLogExtraInfo{ + quota::ScopedLogExtraInfo::kTagQuery, aSQLCommand}; + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(aSQLCommand))); + } + + return NS_OK; +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/ActorsParentCommon.h b/dom/indexedDB/ActorsParentCommon.h new file mode 100644 index 0000000000..d3e65eae82 --- /dev/null +++ b/dom/indexedDB/ActorsParentCommon.h @@ -0,0 +1,119 @@ +/* -*- 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_actorsparentcommon_h__ +#define mozilla_dom_indexeddb_actorsparentcommon_h__ + +// Declares functions and types used locally within IndexedDB, which are defined +// in ActorsParent.cpp + +#include <stdint.h> +#include <tuple> +#include <utility> +#include "ErrorList.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "nscore.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +struct JSContext; +class JSObject; +class mozIStorageConnection; +class mozIStorageStatement; +class mozIStorageValueArray; + +namespace mozilla::dom::indexedDB { + +class DatabaseFileManager; +struct StructuredCloneFileParent; +struct StructuredCloneReadInfoParent; + +extern const nsLiteralString kJournalDirectoryName; + +// At the moment, the encrypted stream block size is assumed to be unchangeable +// between encrypting and decrypting blobs. This assumptions holds as long as we +// only encrypt in private browsing mode, but when we support encryption for +// persistent storage, this needs to be changed. +constexpr uint32_t kEncryptedStreamBlockSize = 4096; + +using IndexOrObjectStoreId = int64_t; + +struct IndexDataValue final { + IndexOrObjectStoreId mIndexId; + Key mPosition; + Key mLocaleAwarePosition; + bool mUnique; + + IndexDataValue(); + +#ifdef NS_BUILD_REFCNT_LOGGING + IndexDataValue(IndexDataValue&& aOther); + + MOZ_COUNTED_DTOR(IndexDataValue) +#endif + + IndexDataValue(IndexOrObjectStoreId aIndexId, bool aUnique, + const Key& aPosition); + + IndexDataValue(IndexOrObjectStoreId aIndexId, bool aUnique, + const Key& aPosition, const Key& aLocaleAwarePosition); + + bool operator==(const IndexDataValue& aOther) const; + + bool operator<(const IndexDataValue& aOther) const; +}; + +JSObject* GetSandbox(JSContext* aCx); + +// The success value of the Result is a a pair of a pointer to the compressed +// index data values buffer and its size. The function does not return a +// nsTArray because the result is typically passed to a function that acquires +// ownership of the pointer. +Result<std::pair<UniqueFreePtr<uint8_t>, uint32_t>, nsresult> +MakeCompressedIndexDataValues(const nsTArray<IndexDataValue>& aIndexValues); + +// aOutIndexValues is an output parameter, since its storage is reused. +nsresult ReadCompressedIndexDataValues( + mozIStorageStatement& aStatement, uint32_t aColumnIndex, + nsTArray<IndexDataValue>& aOutIndexValues); + +using IndexDataValuesAutoArray = AutoTArray<IndexDataValue, 32>; + +template <typename T> +Result<IndexDataValuesAutoArray, nsresult> ReadCompressedIndexDataValues( + T& aValues, uint32_t aColumnIndex); + +Result<std::tuple<IndexOrObjectStoreId, bool, Span<const uint8_t>>, nsresult> +ReadCompressedIndexId(Span<const uint8_t> aData); + +Result<std::pair<uint64_t, mozilla::Span<const uint8_t>>, nsresult> +ReadCompressedNumber(Span<const uint8_t> aSpan); + +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromValueArray( + mozIStorageValueArray* aValues, uint32_t aDataIndex, uint32_t aFileIdsIndex, + const DatabaseFileManager& aFileManager); + +Result<StructuredCloneReadInfoParent, nsresult> +GetStructuredCloneReadInfoFromStatement( + mozIStorageStatement* aStatement, uint32_t aDataIndex, + uint32_t aFileIdsIndex, const DatabaseFileManager& aFileManager); + +Result<nsTArray<StructuredCloneFileParent>, nsresult> +DeserializeStructuredCloneFiles(const DatabaseFileManager& aFileManager, + const nsAString& aText); + +nsresult ExecuteSimpleSQLSequence(mozIStorageConnection& aConnection, + Span<const nsLiteralCString> aSQLCommands); + +} // namespace mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_actorsparent_h__ diff --git a/dom/indexedDB/DBSchema.cpp b/dom/indexedDB/DBSchema.cpp new file mode 100644 index 0000000000..aa7c6338b1 --- /dev/null +++ b/dom/indexedDB/DBSchema.cpp @@ -0,0 +1,174 @@ +/* -*- 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 "DBSchema.h" + +// local includes +#include "ActorsParentCommon.h" +#include "IndexedDBCommon.h" + +// global includes +#include "ErrorList.h" +#include "js/StructuredClone.h" +#include "mozIStorageConnection.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ProfilerLabels.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsLiteralString.h" +#include "nsString.h" + +namespace mozilla::dom::indexedDB { + +using quota::AssertIsOnIOThread; + +// 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."); + +nsresult CreateFileTables(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("CreateFileTables", DOM); + + constexpr nsLiteralCString commands[] = { + // Table `file` + "CREATE TABLE file (" + "id INTEGER PRIMARY KEY, " + "refcount INTEGER NOT NULL" + ");"_ns, + "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;"_ns, + "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;"_ns, + "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;"_ns, + "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;"_ns}; + + QM_TRY(MOZ_TO_RESULT(ExecuteSimpleSQLSequence(aConnection, commands))); + + return NS_OK; +} + +nsresult CreateTables(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("CreateTables", DOM); + + constexpr nsLiteralCString commands[] = { + // 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. + "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;"_ns, + // Table `object_store` + "CREATE TABLE object_store" + "( id INTEGER PRIMARY KEY" + ", auto_increment INTEGER NOT NULL DEFAULT 0" + ", name TEXT NOT NULL" + ", key_path TEXT" + ");"_ns, + // Table `object_store_index` + "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) " + ");"_ns, + // Table `object_data` + "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;"_ns, + // Table `index_data` + "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;"_ns, + "CREATE INDEX index_data_value_locale_index " + "ON index_data (index_id, value_locale, object_data_key, value) " + "WHERE value_locale IS NOT NULL;"_ns, + // Table `unique_index_data` + "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;"_ns, + "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;"_ns}; + + QM_TRY(MOZ_TO_RESULT(ExecuteSimpleSQLSequence(aConnection, commands))); + + QM_TRY(MOZ_TO_RESULT(CreateFileTables(aConnection))); + + QM_TRY(MOZ_TO_RESULT(aConnection.SetSchemaVersion(kSQLiteSchemaVersion))); + + return NS_OK; +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/DBSchema.h b/dom/indexedDB/DBSchema.h new file mode 100644 index 0000000000..0c0a0d9850 --- /dev/null +++ b/dom/indexedDB/DBSchema.h @@ -0,0 +1,46 @@ +/* -*- 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 dom_indexeddb_dbschema_h__ +#define dom_indexeddb_dbschema_h__ + +#include "ErrorList.h" + +#include <cstdint> + +class mozIStorageConnection; + +namespace mozilla::dom::indexedDB { + +// Major schema version. Bump for almost everything. +const uint32_t kMajorSchemaVersion = 26; + +// 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."); + +constexpr int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion, + uint32_t aMinorSchemaVersion) { + return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion); +} + +constexpr int32_t kSQLiteSchemaVersion = + MakeSchemaVersion(kMajorSchemaVersion, kMinorSchemaVersion); + +nsresult CreateFileTables(mozIStorageConnection& aConnection); +nsresult CreateTables(mozIStorageConnection& aConnection); + +} // namespace mozilla::dom::indexedDB + +#endif diff --git a/dom/indexedDB/DatabaseFileInfo.cpp b/dom/indexedDB/DatabaseFileInfo.cpp new file mode 100644 index 0000000000..8df08138d4 --- /dev/null +++ b/dom/indexedDB/DatabaseFileInfo.cpp @@ -0,0 +1,15 @@ +/* -*- 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 "DatabaseFileInfo.h" + +#include "FileInfoImpl.h" + +namespace mozilla::dom::indexedDB { + +template class FileInfo<DatabaseFileManager>; + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/DatabaseFileInfo.h b/dom/indexedDB/DatabaseFileInfo.h new file mode 100644 index 0000000000..a65ead5bd7 --- /dev/null +++ b/dom/indexedDB/DatabaseFileInfo.h @@ -0,0 +1,15 @@ +/* -*- 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 DOM_INDEXEDDB_DATABASEFILEINFO_H_ +#define DOM_INDEXEDDB_DATABASEFILEINFO_H_ + +#include "DatabaseFileInfoFwd.h" + +#include "DatabaseFileManager.h" +#include "FileInfo.h" + +#endif // DOM_INDEXEDDB_DATABASEFILEINFO_H_ diff --git a/dom/indexedDB/DatabaseFileInfoFwd.h b/dom/indexedDB/DatabaseFileInfoFwd.h new file mode 100644 index 0000000000..fb70f14248 --- /dev/null +++ b/dom/indexedDB/DatabaseFileInfoFwd.h @@ -0,0 +1,21 @@ +/* -*- 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 DOM_INDEXEDDB_DATABASEFILEINFOFWD_H_ +#define DOM_INDEXEDDB_DATABASEFILEINFOFWD_H_ + +namespace mozilla::dom::indexedDB { + +class DatabaseFileManager; + +template <typename FileManager> +class FileInfo; + +using DatabaseFileInfo = FileInfo<DatabaseFileManager>; + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_DATABASEFILEINFOFWD_H_ diff --git a/dom/indexedDB/DatabaseFileManager.h b/dom/indexedDB/DatabaseFileManager.h new file mode 100644 index 0000000000..57809cb12b --- /dev/null +++ b/dom/indexedDB/DatabaseFileManager.h @@ -0,0 +1,124 @@ +/* -*- 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 DOM_INDEXEDDB_DATABASEFILEMANAGER_H_ +#define DOM_INDEXEDDB_DATABASEFILEMANAGER_H_ + +#include "FileInfoManager.h" +#include "IndexedDBCipherKeyManager.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/InitializedOnce.h" + +class nsIFile; +class mozIStorageConnection; + +namespace mozilla::dom::indexedDB { + +// Implemented in ActorsParent.cpp. +class DatabaseFileManager final + : public FileInfoManager<DatabaseFileManager>, + public AtomicSafeRefCounted<DatabaseFileManager> { + using PersistenceType = mozilla::dom::quota::PersistenceType; + using FileInfoManager<DatabaseFileManager>::MutexType; + + const PersistenceType mPersistenceType; + const quota::OriginMetadata mOriginMetadata; + const nsString mDatabaseName; + const nsCString mDatabaseID; + + mutable IndexedDBCipherKeyManager mCipherKeyManager; + + LazyInitializedOnce<const nsString> mDirectoryPath; + LazyInitializedOnce<const nsString> mJournalDirectoryPath; + + const bool mEnforcingQuota; + const bool mIsInPrivateBrowsingMode; + + FlippedOnce<false> mInitialized; + + // Lock protecting DatabaseFileManager.mFileInfos. + // It's s also used to atomically update DatabaseFileInfo.mRefCnt and + // DatabaseFileInfo.mDBRefCnt + static MutexType sMutex; + + public: + [[nodiscard]] static nsCOMPtr<nsIFile> GetFileForId(nsIFile* aDirectory, + int64_t aId); + + [[nodiscard]] static nsCOMPtr<nsIFile> GetCheckedFileForId( + nsIFile* aDirectory, int64_t aId); + + static nsresult InitDirectory(nsIFile& aDirectory, nsIFile& aDatabaseFile, + const nsACString& aOrigin, + uint32_t aTelemetryId); + + template <typename KnownDirEntryOp, typename UnknownDirEntryOp> + static Result<Ok, nsresult> TraverseFiles( + nsIFile& aDirectory, KnownDirEntryOp&& aKnownDirEntryOp, + UnknownDirEntryOp&& aUnknownDirEntryOp); + + static Result<quota::FileUsageType, nsresult> GetUsage(nsIFile* aDirectory); + + DatabaseFileManager(PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + const nsAString& aDatabaseName, + const nsCString& aDatabaseID, bool aEnforcingQuota, + bool aIsInPrivateBrowsingMode); + + PersistenceType Type() const { return mPersistenceType; } + + const quota::OriginMetadata& OriginMetadata() const { + return mOriginMetadata; + } + + const nsACString& Origin() const { return mOriginMetadata.mOrigin; } + + const nsAString& DatabaseName() const { return mDatabaseName; } + + const nsCString& DatabaseID() const { return mDatabaseID; } + + IndexedDBCipherKeyManager& MutableCipherKeyManagerRef() const { + return mCipherKeyManager; + } + + auto IsInPrivateBrowsingMode() const { return mIsInPrivateBrowsingMode; } + + bool EnforcingQuota() const { return mEnforcingQuota; } + + bool Initialized() const { return mInitialized; } + + nsresult Init(nsIFile* aDirectory, mozIStorageConnection& aConnection); + + [[nodiscard]] nsCOMPtr<nsIFile> GetDirectory(); + + [[nodiscard]] nsCOMPtr<nsIFile> GetCheckedDirectory(); + + [[nodiscard]] nsCOMPtr<nsIFile> GetJournalDirectory(); + + [[nodiscard]] nsCOMPtr<nsIFile> EnsureJournalDirectory(); + + [[nodiscard]] nsresult SyncDeleteFile(int64_t aId); + + // XXX When getting rid of FileHelper, this method should be removed/made + // private. + [[nodiscard]] nsresult SyncDeleteFile(nsIFile& aFile, + nsIFile& aJournalFile) const; + + [[nodiscard]] nsresult AsyncDeleteFile(int64_t aFileId); + + MOZ_DECLARE_REFCOUNTED_TYPENAME(DatabaseFileManager) + + static StaticMutex& Mutex() { return sMutex; } + + ~DatabaseFileManager() = default; +}; + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_DATABASEFILEMANAGER_H_ diff --git a/dom/indexedDB/DatabaseFileManagerImpl.h b/dom/indexedDB/DatabaseFileManagerImpl.h new file mode 100644 index 0000000000..0753adf5ff --- /dev/null +++ b/dom/indexedDB/DatabaseFileManagerImpl.h @@ -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/. */ + +#ifndef DOM_INDEXEDDB_DATABASEFILEMANAGERIMPL_H_ +#define DOM_INDEXEDDB_DATABASEFILEMANAGERIMPL_H_ + +#include "DatabaseFileManager.h" + +// local includes +#include "ActorsParentCommon.h" + +// global includes +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "nsIFile.h" +#include "nsString.h" + +namespace mozilla::dom::indexedDB { + +template <typename KnownDirEntryOp, typename UnknownDirEntryOp> +Result<Ok, nsresult> DatabaseFileManager::TraverseFiles( + nsIFile& aDirectory, KnownDirEntryOp&& aKnownDirEntryOp, + UnknownDirEntryOp&& aUnknownDirEntryOp) { + quota::AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); + + if (!exists) { + return Ok{}; + } + + QM_TRY(quota::CollectEachFile( + aDirectory, + [&aKnownDirEntryOp, &aUnknownDirEntryOp]( + const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, quota::GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case quota::nsIFileKind::ExistsAsDirectory: { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); + + if (leafName.Equals(kJournalDirectoryName)) { + QM_TRY(std::forward<KnownDirEntryOp>(aKnownDirEntryOp)( + *file, /* isDirectory */ true)); + + break; + } + + Unused << WARN_IF_FILE_IS_UNKNOWN(*file); + + QM_TRY(std::forward<UnknownDirEntryOp>(aUnknownDirEntryOp)( + *file, /* isDirectory */ true)); + + break; + } + + case quota::nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName)); + + nsresult rv; + leafName.ToInteger64(&rv); + if (NS_SUCCEEDED(rv)) { + QM_TRY(std::forward<KnownDirEntryOp>(aKnownDirEntryOp)( + *file, /* isDirectory */ false)); + + break; + } + + Unused << WARN_IF_FILE_IS_UNKNOWN(*file); + + QM_TRY(std::forward<UnknownDirEntryOp>(aUnknownDirEntryOp)( + *file, /* isDirectory */ false)); + + break; + } + + case quota::nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + })); + + return Ok{}; +} + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_DATABASEFILEMANAGERIMPL_H_ diff --git a/dom/indexedDB/FileInfo.h b/dom/indexedDB/FileInfo.h new file mode 100644 index 0000000000..392ab3c96a --- /dev/null +++ b/dom/indexedDB/FileInfo.h @@ -0,0 +1,69 @@ +/* -*- 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 DOM_INDEXEDDB_FILEINFO_H_ +#define DOM_INDEXEDDB_FILEINFO_H_ + +#include "nsISupportsImpl.h" +#include "nsCOMPtr.h" +#include "SafeRefPtr.h" + +namespace mozilla::dom::indexedDB { + +class FileInfoBase { + public: + using IdType = int64_t; + + IdType Id() const { return mFileId; } + + protected: + explicit FileInfoBase(const int64_t aFileId) : mFileId(aFileId) { + MOZ_ASSERT(mFileId > 0); + } + + private: + const IdType mFileId; +}; + +template <typename FileManager> +class FileInfo final : public FileInfoBase { + public: + using AutoLockType = typename FileManager::AutoLockType; + + FileInfo(const typename FileManager::FileInfoManagerGuard& aGuard, + SafeRefPtr<FileManager> aFileManager, const int64_t aFileId, + const nsrefcnt aInitialDBRefCnt = 0); + + void AddRef(); + void Release(const bool aSyncDeleteFile = false); + + void UpdateDBRefs(int32_t aDelta); + + void GetReferences(int32_t* aRefCnt, int32_t* aDBRefCnt); + + FileManager& Manager() const; + + nsCOMPtr<nsIFile> GetFileForFileInfo() const; + + void LockedAddRef(); + bool LockedClearDBRefs( + const typename FileManager::FileInfoManagerGuard& aGuard); + + private: + void UpdateReferences(ThreadSafeAutoRefCnt& aRefCount, int32_t aDelta, + bool aSyncDeleteFile = false); + + void Cleanup(); + + ThreadSafeAutoRefCnt mRefCnt; + ThreadSafeAutoRefCnt mDBRefCnt; + + const SafeRefPtr<FileManager> mFileManager; +}; + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_FILEINFO_H_ diff --git a/dom/indexedDB/FileInfoImpl.h b/dom/indexedDB/FileInfoImpl.h new file mode 100644 index 0000000000..b7ffa3be8c --- /dev/null +++ b/dom/indexedDB/FileInfoImpl.h @@ -0,0 +1,151 @@ +/* -*- 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 DOM_INDEXEDDB_FILEINFOIMPL_H_ +#define DOM_INDEXEDDB_FILEINFOIMPL_H_ + +#include "FileInfo.h" + +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/Mutex.h" +#include "nsIFile.h" + +namespace mozilla::dom::indexedDB { + +template <typename FileManager> +FileInfo<FileManager>::FileInfo( + const typename FileManager::FileInfoManagerGuard& aGuard, + SafeRefPtr<FileManager> aFileManager, const int64_t aFileId, + const nsrefcnt aInitialDBRefCnt) + : FileInfoBase{aFileId}, + mDBRefCnt(aInitialDBRefCnt), + mFileManager(std::move(aFileManager)) { + MOZ_ASSERT(mFileManager); +} + +template <typename FileManager> +void FileInfo<FileManager>::AddRef() { + AutoLockType lock(FileManager::Mutex()); + + LockedAddRef(); +} + +template <typename FileManager> +void FileInfo<FileManager>::Release(const bool aSyncDeleteFile) { + UpdateReferences(mRefCnt, -1, aSyncDeleteFile); +} + +template <typename FileManager> +void FileInfo<FileManager>::UpdateDBRefs(int32_t aDelta) { + UpdateReferences(mDBRefCnt, aDelta); +} + +template <typename FileManager> +void FileInfo<FileManager>::GetReferences(int32_t* const aRefCnt, + int32_t* const aDBRefCnt) { + AutoLockType lock(FileManager::Mutex()); + + if (aRefCnt) { + *aRefCnt = mRefCnt; + } + + if (aDBRefCnt) { + *aDBRefCnt = mDBRefCnt; + } +} + +template <typename FileManager> +FileManager& FileInfo<FileManager>::Manager() const { + return *mFileManager; +} + +template <typename FileManager> +void FileInfo<FileManager>::UpdateReferences(ThreadSafeAutoRefCnt& aRefCount, + const int32_t aDelta, + const bool aSyncDeleteFile) { + bool needsCleanup; + { + AutoLockType lock(FileManager::Mutex()); + + aRefCount = aRefCount + aDelta; + + if (mRefCnt + mDBRefCnt > 0) { + return; + } + + mFileManager->RemoveFileInfo(Id(), lock); + + // If the FileManager was already invalidated, we don't need to do any + // cleanup anymore. In that case, the entire origin directory has already + // been deleted by the quota manager, and we don't need to delete individual + // files. + needsCleanup = !mFileManager->Invalidated(); + } + + if (needsCleanup) { + if (aSyncDeleteFile) { + QM_WARNONLY_TRY(QM_TO_RESULT(mFileManager->SyncDeleteFile(Id()))); + } else { + Cleanup(); + } + } + + delete this; +} + +template <typename FileManager> +void FileInfo<FileManager>::LockedAddRef() { + FileManager::Mutex().AssertCurrentThreadOwns(); + + ++mRefCnt; +} + +template <typename FileManager> +bool FileInfo<FileManager>::LockedClearDBRefs( + const typename FileManager::FileInfoManagerGuard&) { + FileManager::Mutex().AssertCurrentThreadOwns(); + + mDBRefCnt = 0; + + if (mRefCnt) { + return true; + } + + // In this case, we are not responsible for removing the FileInfo from the + // hashtable. It's up to FileManager which is the only caller of this method. + + MOZ_ASSERT(mFileManager->Invalidated()); + + delete this; + + return false; +} + +template <typename FileManager> +void FileInfo<FileManager>::Cleanup() { + QM_WARNONLY_TRY(QM_TO_RESULT(mFileManager->AsyncDeleteFile(Id()))); +} + +template <typename FileManager> +nsCOMPtr<nsIFile> FileInfo<FileManager>::GetFileForFileInfo() const { + const nsCOMPtr<nsIFile> directory = Manager().GetDirectory(); + if (NS_WARN_IF(!directory)) { + return nullptr; + } + + nsCOMPtr<nsIFile> file = FileManager::GetFileForId(directory, Id()); + if (NS_WARN_IF(!file)) { + return nullptr; + } + + return file; +} + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_FILEINFOIMPL_H_ diff --git a/dom/indexedDB/FileInfoManager.h b/dom/indexedDB/FileInfoManager.h new file mode 100644 index 0000000000..eff4351386 --- /dev/null +++ b/dom/indexedDB/FileInfoManager.h @@ -0,0 +1,138 @@ +/* -*- 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 DOM_INDEXEDDB_FILEINFOMANAGER_H_ +#define DOM_INDEXEDDB_FILEINFOMANAGER_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include "mozilla/StaticMutex.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "nsISupportsImpl.h" +#include "FileInfo.h" +#include "FlippedOnce.h" + +namespace mozilla::dom::indexedDB { + +class FileInfoManagerBase { + public: + bool Invalidated() const { return mInvalidated; } + + protected: + bool AssertValid() const { + if (NS_WARN_IF(Invalidated())) { + MOZ_ASSERT(false); + return false; + } + + return true; + } + + void Invalidate() { mInvalidated.Flip(); } + + private: + FlippedOnce<false> mInvalidated; +}; + +template <typename FileManager> +class FileInfoManager : public FileInfoManagerBase { + public: + using FileInfoType = FileInfo<FileManager>; + using MutexType = StaticMutex; + using AutoLockType = mozilla::detail::BaseAutoLock<MutexType&>; + + [[nodiscard]] SafeRefPtr<FileInfoType> GetFileInfo(int64_t aId) const { + return AcquireFileInfo([this, aId] { return mFileInfos.MaybeGet(aId); }); + } + + [[nodiscard]] SafeRefPtr<FileInfoType> CreateFileInfo() { + return AcquireFileInfo([this] { + const int64_t id = ++mLastFileId; + + auto fileInfo = + MakeNotNull<FileInfoType*>(FileInfoManagerGuard{}, + SafeRefPtr{static_cast<FileManager*>(this), + AcquireStrongRefFromRawPtr{}}, + id); + + mFileInfos.InsertOrUpdate(id, fileInfo); + return Some(fileInfo); + }); + } + + void RemoveFileInfo(const int64_t aId, const AutoLockType& aFileMutexLock) { +#ifdef DEBUG + aFileMutexLock.AssertOwns(FileManager::Mutex()); +#endif + mFileInfos.Remove(aId); + } + + nsresult Invalidate() { + AutoLockType lock(FileManager::Mutex()); + + FileInfoManagerBase::Invalidate(); + + mFileInfos.RemoveIf([](const auto& iter) { + FileInfoType* info = iter.Data(); + MOZ_ASSERT(info); + + return !info->LockedClearDBRefs(FileInfoManagerGuard{}); + }); + + return NS_OK; + } + + struct FileInfoManagerGuard { + FileInfoManagerGuard() = default; + }; + + private: + // Runs the given aFileInfoTableOp operation, which must return a FileInfo*, + // under the FileManager lock, acquires a strong reference to the returned + // object under the lock, and returns the strong reference. + template <typename FileInfoTableOp> + [[nodiscard]] SafeRefPtr<FileInfoType> AcquireFileInfo( + const FileInfoTableOp& aFileInfoTableOp) const { + if (!AssertValid()) { + // In release, the assertions are disabled. + return nullptr; + } + + // We cannot simply change this to SafeRefPtr<FileInfo>, because + // FileInfo::AddRef also acquires the FileManager::Mutex. + auto fileInfo = [&aFileInfoTableOp]() -> RefPtr<FileInfoType> { + AutoLockType lock(FileManager::Mutex()); + + const auto maybeFileInfo = aFileInfoTableOp(); + if (maybeFileInfo) { + const auto& fileInfo = maybeFileInfo.ref(); + fileInfo->LockedAddRef(); + return dont_AddRef(fileInfo.get()); + } + + return {}; + }(); + + return SafeRefPtr{std::move(fileInfo)}; + } + + protected: +#ifdef DEBUG + ~FileInfoManager() { MOZ_ASSERT(mFileInfos.IsEmpty()); } +#else + ~FileInfoManager() = default; +#endif + + // Access to the following fields must be protected by + // FileManager::Mutex() + int64_t mLastFileId = 0; + nsTHashMap<nsUint64HashKey, NotNull<FileInfoType*>> mFileInfos; +}; + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_FILEINFOMANAGER_H_ diff --git a/dom/indexedDB/FlippedOnce.h b/dom/indexedDB/FlippedOnce.h new file mode 100644 index 0000000000..9ef2383a7c --- /dev/null +++ b/dom/indexedDB/FlippedOnce.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_flippedonce_h__ +#define mozilla_dom_indexeddb_flippedonce_h__ + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" + +namespace mozilla { + +// A state-restricted bool, which can only be flipped once. It isn't required to +// be flipped during its lifetime. +template <bool Initial> +class FlippedOnce { + public: + FlippedOnce(const FlippedOnce&) = delete; + FlippedOnce& operator=(const FlippedOnce&) = delete; + FlippedOnce(FlippedOnce&&) = default; + FlippedOnce& operator=(FlippedOnce&&) = default; + + constexpr FlippedOnce() = default; + + MOZ_IMPLICIT constexpr operator bool() const { return mValue; }; + + constexpr void Flip() { + MOZ_ASSERT(mValue == Initial); + EnsureFlipped(); + } + + constexpr void EnsureFlipped() { mValue = !Initial; } + + private: + bool mValue = Initial; +}; +} // namespace mozilla + +#endif diff --git a/dom/indexedDB/IDBCursor.cpp b/dom/indexedDB/IDBCursor.cpp new file mode 100644 index 0000000000..2fb6e98715 --- /dev/null +++ b/dom/indexedDB/IDBCursor.cpp @@ -0,0 +1,875 @@ +/* -*- 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/HoldDropJSObjects.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::dom { + +using namespace indexedDB; + +IDBCursor::IDBCursor(BackgroundCursorChildBase* const aBackgroundActor) + : mBackgroundActor(WrapNotNull(aBackgroundActor)), + mRequest(aBackgroundActor->GetRequest()), + mTransaction(&mRequest->MutableTransactionRef()), + mCachedKey(JS::UndefinedValue()), + mCachedPrimaryKey(JS::UndefinedValue()), + mCachedValue(JS::UndefinedValue()), + mDirection(aBackgroundActor->GetDirection()), + mHaveCachedKey(false), + mHaveCachedPrimaryKey(false), + mHaveCachedValue(false), + mRooted(false), + mContinueCalled(false), + mHaveValue(true) { + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(mRequest); + + mTransaction->RegisterCursor(*this); +} + +template <IDBCursor::Type CursorType> +IDBTypedCursor<CursorType>::~IDBTypedCursor() { + AssertIsOnOwningThread(); + + mTransaction->UnregisterCursor(*this); + + DropJSObjects(); + + if (mBackgroundActor) { + (*mBackgroundActor)->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } + + // Let's explicitly not leave any dangling CheckedUnsafePtr. + mTransaction = nullptr; +} + +// static +RefPtr<IDBObjectStoreCursor> IDBCursor::Create( + BackgroundCursorChild<Type::ObjectStore>* const aBackgroundActor, Key aKey, + StructuredCloneReadInfoChild&& aCloneInfo) { + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(!aKey.IsUnset()); + + return MakeRefPtr<IDBObjectStoreCursor>(aBackgroundActor, std::move(aKey), + std::move(aCloneInfo)); +} + +// static +RefPtr<IDBObjectStoreKeyCursor> IDBCursor::Create( + BackgroundCursorChild<Type::ObjectStoreKey>* const aBackgroundActor, + Key aKey) { + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(!aKey.IsUnset()); + + return MakeRefPtr<IDBObjectStoreKeyCursor>(aBackgroundActor, std::move(aKey)); +} + +// static +RefPtr<IDBIndexCursor> IDBCursor::Create( + BackgroundCursorChild<Type::Index>* const aBackgroundActor, Key aKey, + Key aSortKey, Key aPrimaryKey, StructuredCloneReadInfoChild&& aCloneInfo) { + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(!aKey.IsUnset()); + MOZ_ASSERT(!aPrimaryKey.IsUnset()); + + return MakeRefPtr<IDBIndexCursor>(aBackgroundActor, std::move(aKey), + std::move(aSortKey), std::move(aPrimaryKey), + std::move(aCloneInfo)); +} + +// static +RefPtr<IDBIndexKeyCursor> IDBCursor::Create( + BackgroundCursorChild<Type::IndexKey>* const aBackgroundActor, Key aKey, + Key aSortKey, Key aPrimaryKey) { + MOZ_ASSERT(aBackgroundActor); + aBackgroundActor->AssertIsOnOwningThread(); + MOZ_ASSERT(!aKey.IsUnset()); + MOZ_ASSERT(!aPrimaryKey.IsUnset()); + + return MakeRefPtr<IDBIndexKeyCursor>(aBackgroundActor, std::move(aKey), + std::move(aSortKey), + std::move(aPrimaryKey)); +} + +#ifdef DEBUG + +void IDBCursor::AssertIsOnOwningThread() const { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::DropJSObjects() { + AssertIsOnOwningThread(); + + Reset(); + + if (!mRooted) { + return; + } + + mRooted = false; + + mozilla::DropJSObjects(this); +} + +template <IDBCursor::Type CursorType> +bool IDBTypedCursor<CursorType>::IsSourceDeleted() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + MOZ_ASSERT(mTransaction->IsActive()); + + const auto* const sourceObjectStore = [this]() -> const IDBObjectStore* { + if constexpr (IsObjectStoreCursor) { + return mSource; + } else { + if (GetSourceRef().IsDeleted()) { + return nullptr; + } + + const auto* const res = GetSourceRef().ObjectStore(); + MOZ_ASSERT(res); + return res; + } + }(); + + return !sourceObjectStore || sourceObjectStore->IsDeleted(); +} + +void IDBCursor::ResetBase() { + AssertIsOnOwningThread(); + + mCachedKey.setUndefined(); + mCachedPrimaryKey.setUndefined(); + mCachedValue.setUndefined(); + + mHaveCachedKey = false; + mHaveCachedPrimaryKey = false; + mHaveCachedValue = false; + mHaveValue = false; + mContinueCalled = false; +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::Reset() { + AssertIsOnOwningThread(); + + if constexpr (!IsKeyOnlyCursor) { + IDBObjectStore::ClearCloneReadInfo(mData.mCloneInfo); + } + + ResetBase(); +} + +nsIGlobalObject* IDBCursor::GetParentObject() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mTransaction); + + return mTransaction->GetParentObject(); +} + +IDBCursorDirection IDBCursor::GetDirection() const { + AssertIsOnOwningThread(); + + switch (mDirection) { + case Direction::Next: + return IDBCursorDirection::Next; + + case Direction::Nextunique: + return IDBCursorDirection::Nextunique; + + case Direction::Prev: + return IDBCursorDirection::Prev; + + case Direction::Prevunique: + return IDBCursorDirection::Prevunique; + + default: + MOZ_CRASH("Bad direction!"); + } +} + +RefPtr<IDBRequest> IDBCursor::Request() const { + AssertIsOnOwningThread(); + return mRequest; +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::GetSource( + OwningIDBObjectStoreOrIDBIndex& aSource) const { + AssertIsOnOwningThread(); + + if constexpr (IsObjectStoreCursor) { + aSource.SetAsIDBObjectStore() = mSource; + } else { + aSource.SetAsIDBIndex() = mSource; + } +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::GetKey(JSContext* const aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mData.mKey.IsUnset() || !mHaveValue); + + if (!mHaveValue) { + aResult.setUndefined(); + return; + } + + if (!mHaveCachedKey) { + if (!mRooted) { + mozilla::HoldJSObjects(this); + mRooted = true; + } + + aRv = mData.mKey.ToJSVal(aCx, mCachedKey); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mHaveCachedKey = true; + } + + aResult.set(mCachedKey); +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::GetPrimaryKey( + JSContext* const 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 = mData.GetObjectStoreKey(); + + MOZ_ASSERT(!key.IsUnset()); + + aRv = key.ToJSVal(aCx, mCachedPrimaryKey); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mHaveCachedPrimaryKey = true; + } + + aResult.set(mCachedPrimaryKey); +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::GetValue(JSContext* const aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if constexpr (!IsKeyOnlyCursor) { + 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, std::move(mData.mCloneInfo), &val))) { + aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); + return; + } + + // XXX This seems redundant, sine mData.mCloneInfo is moved above. + IDBObjectStore::ClearCloneReadInfo(mData.mCloneInfo); + + mCachedValue = val; + mHaveCachedValue = true; + } + + aResult.set(mCachedValue); + } else { + MOZ_CRASH("This shouldn't be callable on a key-only cursor."); + } +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::Continue(JSContext* const aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (!mTransaction->IsActive()) { + 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; + auto result = key.SetFromJSVal(aCx, aKey); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return; + } + + if constexpr (!IsObjectStoreCursor) { + if (IsLocaleAware() && !key.IsUnset()) { + auto result = key.ToLocaleAwareKey(GetSourceRef().Locale()); + if (result.isErr()) { + aRv.Throw(result.inspectErr()); + return; + } + key = result.unwrap(); + } + } + + const Key& sortKey = mData.GetSortKey(IsLocaleAware()); + + if (!key.IsUnset()) { + switch (mDirection) { + case Direction::Next: + case Direction::Nextunique: + if (key <= sortKey) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + break; + + case Direction::Prev: + case Direction::Prevunique: + 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 constexpr (IsObjectStoreCursor) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).continue(%s)", + "IDBCursor.continue(%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(mSource), + IDB_LOG_STRINGIFY(mDirection), IDB_LOG_STRINGIFY(key)); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).continue(%s)", + "IDBCursor.continue(%.0s%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), + IDB_LOG_STRINGIFY(GetSourceRef().ObjectStore()), + IDB_LOG_STRINGIFY(mSource), IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(key)); + } + + GetTypedBackgroundActorRef().SendContinueInternal(ContinueParams(key), mData); + + mContinueCalled = true; +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::ContinuePrimaryKey( + JSContext* const aCx, JS::Handle<JS::Value> aKey, + JS::Handle<JS::Value> aPrimaryKey, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (!mTransaction->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + if (IsSourceDeleted()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + if (IsObjectStoreCursor || + (mDirection != Direction::Next && mDirection != Direction::Prev)) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return; + } + + if constexpr (!IsObjectStoreCursor) { + if (!mHaveValue || mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + Key key; + auto result = key.SetFromJSVal(aCx, aKey); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return; + } + + if (IsLocaleAware() && !key.IsUnset()) { + auto result = key.ToLocaleAwareKey(GetSourceRef().Locale()); + if (result.isErr()) { + aRv.Throw(result.inspectErr()); + return; + } + key = result.unwrap(); + } + + if (key.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + + Key primaryKey; + result = primaryKey.SetFromJSVal(aCx, aPrimaryKey); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return; + } + + if (primaryKey.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + + const Key& sortKey = mData.GetSortKey(IsLocaleAware()); + + switch (mDirection) { + case Direction::Next: + if (key < sortKey || + (key == sortKey && primaryKey <= mData.mObjectStoreKey)) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + break; + + case Direction::Prev: + if (key > sortKey || + (key == sortKey && primaryKey >= mData.mObjectStoreKey)) { + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).continuePrimaryKey(%s, %s)", + "IDBCursor.continuePrimaryKey(%.0s%.0s%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), + IDB_LOG_STRINGIFY(&GetSourceObjectStoreRef()), + IDB_LOG_STRINGIFY(mSource), IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(key), IDB_LOG_STRINGIFY(primaryKey)); + + GetTypedBackgroundActorRef().SendContinueInternal( + ContinuePrimaryKeyParams(key, primaryKey), mData); + + mContinueCalled = true; + } +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::Advance(const uint32_t aCount, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (!aCount) { + aRv.ThrowTypeError("0 (Zero) is not a valid advance count."); + return; + } + + if (!mTransaction->IsActive()) { + 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 constexpr (IsObjectStoreCursor) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).advance(%" PRIi32 ")", + "IDBCursor.advance(%.0s%.0s%.0s%.0s%" PRIi32 ")", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(mSource), + IDB_LOG_STRINGIFY(mDirection), aCount); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).advance(%" PRIi32 ")", + "IDBCursor.advance(%.0s%.0s%.0s%.0s%.0s%" PRIi32 ")", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), + IDB_LOG_STRINGIFY(GetSourceRef().ObjectStore()), + IDB_LOG_STRINGIFY(mSource), IDB_LOG_STRINGIFY(mDirection), aCount); + } + + GetTypedBackgroundActorRef().SendContinueInternal(AdvanceParams(aCount), + mData); + + mContinueCalled = true; +} + +template <IDBCursor::Type CursorType> +RefPtr<IDBRequest> IDBTypedCursor<CursorType>::Update( + JSContext* const aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (!mTransaction->IsActive()) { + 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::Mode::Cleanup || + IsSourceDeleted() || !mHaveValue || IsKeyOnlyCursor || mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if constexpr (!IsKeyOnlyCursor) { + MOZ_ASSERT(!mData.mKey.IsUnset()); + if constexpr (!IsObjectStoreCursor) { + MOZ_ASSERT(!mData.mObjectStoreKey.IsUnset()); + } + + mTransaction->InvalidateCursorCaches(); + + IDBObjectStore::ValueWrapper valueWrapper(aCx, aValue); + + const Key& primaryKey = mData.GetObjectStoreKey(); + + RefPtr<IDBRequest> request; + + IDBObjectStore& objectStore = GetSourceObjectStoreRef(); + if (objectStore.HasValidKeyPath()) { + if (!valueWrapper.Clone(aCx)) { + aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); + return nullptr; + } + + // Make sure the object given has the correct keyPath value set on it. + const KeyPath& keyPath = objectStore.GetKeyPath(); + Key key; + + aRv = keyPath.ExtractKey(aCx, valueWrapper.Value(), key); + if (aRv.Failed()) { + return nullptr; + } + + if (key != primaryKey) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + request = objectStore.AddOrPut(aCx, valueWrapper, + /* 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, valueWrapper, keyVal, + /* aOverwrite */ true, + /* aFromCursor */ true, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + request->SetSource(this); + + if (IsObjectStoreCursor) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).update(%s)", + "IDBCursor.update(%.0s%.0s%.0s%.0s%.0s)", + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).update(%s)", + "IDBCursor.update(%.0s%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(&objectStore), + IDB_LOG_STRINGIFY(mSource), IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(&objectStore, primaryKey)); + } + + return request; + } else { + // XXX: Just to work around a bug in gcc, which otherwise claims 'control + // reaches end of non-void function', which is not true. + return nullptr; + } +} + +template <IDBCursor::Type CursorType> +RefPtr<IDBRequest> IDBTypedCursor<CursorType>::Delete(JSContext* const aCx, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (!mTransaction->IsActive()) { + 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 || IsKeyOnlyCursor || mContinueCalled) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if constexpr (!IsKeyOnlyCursor) { + MOZ_ASSERT(!mData.mKey.IsUnset()); + + mTransaction->InvalidateCursorCaches(); + + const Key& primaryKey = mData.GetObjectStoreKey(); + + JS::Rooted<JS::Value> key(aCx); + aRv = primaryKey.ToJSVal(aCx, &key); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + auto& objectStore = GetSourceObjectStoreRef(); + RefPtr<IDBRequest> request = + objectStore.DeleteInternal(aCx, key, /* aFromCursor */ true, aRv); + if (aRv.Failed()) { + return nullptr; + } + + request->SetSource(this); + + if (IsObjectStoreCursor) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "cursor(%s).delete(%s)", + "IDBCursor.delete(%.0s%.0s%.0s%.0s%.0s)", + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "index(%s).cursor(%s).delete(%s)", + "IDBCursor.delete(%.0s%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(&objectStore), + IDB_LOG_STRINGIFY(mSource), IDB_LOG_STRINGIFY(mDirection), + IDB_LOG_STRINGIFY(&objectStore, primaryKey)); + } + + return request; + } else { + // XXX: Just to work around a bug in gcc, which otherwise claims 'control + // reaches end of non-void function', which is not true. + return nullptr; + } +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::Reset(CursorData<CursorType>&& aCursorData) { + this->AssertIsOnOwningThread(); + + Reset(); + + mData = std::move(aCursorData); + + mHaveValue = !mData.mKey.IsUnset(); +} + +template <IDBCursor::Type CursorType> +void IDBTypedCursor<CursorType>::InvalidateCachedResponses() { + AssertIsOnOwningThread(); + + // TODO: Can mBackgroundActor actually be empty at this point? + if (mBackgroundActor) { + GetTypedBackgroundActorRef().InvalidateCachedResponses(); + } +} + +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(mRequest) +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(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) +// Unlinking is done in the subclasses. +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +// Don't unlink mRequest or mSource in +// NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED! +#define NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS_METHODS(_subclassName) \ + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(_subclassName, IDBCursor) \ + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSource) \ + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END \ + \ + NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(_subclassName, IDBCursor) \ + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER \ + tmp->DropJSObjects(); \ + NS_IMPL_CYCLE_COLLECTION_UNLINK_END \ + \ + NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(_subclassName) \ + NS_INTERFACE_MAP_END_INHERITING(IDBCursor) \ + \ + NS_IMPL_ADDREF_INHERITED(_subclassName, IDBCursor) \ + NS_IMPL_RELEASE_INHERITED(_subclassName, IDBCursor) + +#define NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS(_subclassName) \ + NS_IMPL_CYCLE_COLLECTION_CLASS(_subclassName) \ + NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS_METHODS(_subclassName) + +NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS(IDBObjectStoreCursor) +NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS(IDBObjectStoreKeyCursor) +NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS(IDBIndexCursor) +NS_IMPL_CYCLE_COLLECTION_IDBCURSOR_SUBCLASS(IDBIndexKeyCursor) + +template <IDBCursor::Type CursorType> +JSObject* IDBTypedCursor<CursorType>::WrapObject( + JSContext* const aCx, JS::Handle<JSObject*> aGivenProto) { + AssertIsOnOwningThread(); + + return IsKeyOnlyCursor + ? IDBCursor_Binding::Wrap(aCx, this, aGivenProto) + : IDBCursorWithValue_Binding::Wrap(aCx, this, aGivenProto); +} + +template <IDBCursor::Type CursorType> +template <typename... DataArgs> +IDBTypedCursor<CursorType>::IDBTypedCursor( + indexedDB::BackgroundCursorChild<CursorType>* const aBackgroundActor, + DataArgs&&... aDataArgs) + : IDBCursor{aBackgroundActor}, + mData{std::forward<DataArgs>(aDataArgs)...}, + mSource(aBackgroundActor->GetSource()) {} + +template <IDBCursor::Type CursorType> +bool IDBTypedCursor<CursorType>::IsLocaleAware() const { + if constexpr (IsObjectStoreCursor) { + return false; + } else { + return !GetSourceRef().Locale().IsEmpty(); + } +} + +template class IDBTypedCursor<IDBCursorType::ObjectStore>; +template class IDBTypedCursor<IDBCursorType::ObjectStoreKey>; +template class IDBTypedCursor<IDBCursorType::Index>; +template class IDBTypedCursor<IDBCursorType::IndexKey>; + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBCursor.h b/dom/indexedDB/IDBCursor.h new file mode 100644 index 0000000000..e719c024ec --- /dev/null +++ b/dom/indexedDB/IDBCursor.h @@ -0,0 +1,289 @@ +/* -*- 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 "IDBCursorType.h" +#include "IndexedDatabase.h" +#include "js/RootingAPI.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IDBTransaction.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/InitializedOnce.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class IDBIndex; +class IDBObjectStore; +class IDBRequest; +class OwningIDBObjectStoreOrIDBIndex; + +class IDBObjectStoreCursor; +class IDBObjectStoreKeyCursor; +class IDBIndexCursor; +class IDBIndexKeyCursor; + +namespace indexedDB { +class BackgroundCursorChildBase; +template <IDBCursorType CursorType> +class BackgroundCursorChild; +} // namespace indexedDB + +class IDBCursor : public nsISupports, public nsWrapperCache { + public: + using Key = indexedDB::Key; + using StructuredCloneReadInfoChild = indexedDB::StructuredCloneReadInfoChild; + + using Direction = IDBCursorDirection; + using Type = IDBCursorType; + + protected: + InitializedOnce<const NotNull<indexedDB::BackgroundCursorChildBase*>> + mBackgroundActor; + + // TODO: mRequest could be made const if Bug 1575173 is resolved. It is + // initialized in the constructor and never modified/cleared. + RefPtr<IDBRequest> mRequest; + + // Sub-classes' mSource will hold this alive. + CheckedUnsafePtr<IDBTransaction> mTransaction; + + protected: + // These are cycle-collected! + JS::Heap<JS::Value> mCachedKey; + JS::Heap<JS::Value> mCachedPrimaryKey; + JS::Heap<JS::Value> mCachedValue; + + const Direction mDirection; + + bool mHaveCachedKey : 1; + bool mHaveCachedPrimaryKey : 1; + bool mHaveCachedValue : 1; + bool mRooted : 1; + bool mContinueCalled : 1; + bool mHaveValue : 1; + + public: + [[nodiscard]] static RefPtr<IDBObjectStoreCursor> Create( + indexedDB::BackgroundCursorChild<Type::ObjectStore>* aBackgroundActor, + Key aKey, StructuredCloneReadInfoChild&& aCloneInfo); + + [[nodiscard]] static RefPtr<IDBObjectStoreKeyCursor> Create( + indexedDB::BackgroundCursorChild<Type::ObjectStoreKey>* aBackgroundActor, + Key aKey); + + [[nodiscard]] static RefPtr<IDBIndexCursor> Create( + indexedDB::BackgroundCursorChild<Type::Index>* aBackgroundActor, Key aKey, + Key aSortKey, Key aPrimaryKey, StructuredCloneReadInfoChild&& aCloneInfo); + + [[nodiscard]] static RefPtr<IDBIndexKeyCursor> Create( + indexedDB::BackgroundCursorChild<Type::IndexKey>* aBackgroundActor, + Key aKey, Key aSortKey, Key aPrimaryKey); + + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + nsIGlobalObject* GetParentObject() const; + + // XXX: The virtual methods that are used by the DOM binding could be removed + // on the base class, if we provided a non-polymorphic wrapper instead, which + // uses a custom dispatch to the actual implementation type. Don't know if + // this is worth it. + + virtual void GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const = 0; + + IDBCursorDirection GetDirection() const; + + RefPtr<IDBRequest> Request() const; + + virtual void GetKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) = 0; + + virtual void GetPrimaryKey(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) = 0; + + // XXX: We could move this to a sub-class, since this is only present on + // IDBCursorWithValue. + virtual void GetValue(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) = 0; + + virtual void Continue(JSContext* aCx, JS::Handle<JS::Value> aKey, + ErrorResult& aRv) = 0; + + virtual void ContinuePrimaryKey(JSContext* aCx, JS::Handle<JS::Value> aKey, + JS::Handle<JS::Value> aPrimaryKey, + ErrorResult& aRv) = 0; + + virtual void Advance(uint32_t aCount, ErrorResult& aRv) = 0; + + [[nodiscard]] virtual RefPtr<IDBRequest> Update(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) = 0; + + [[nodiscard]] virtual RefPtr<IDBRequest> Delete(JSContext* aCx, + ErrorResult& aRv) = 0; + + void ClearBackgroundActor() { + AssertIsOnOwningThread(); + + mBackgroundActor.destroy(); + } + + virtual void InvalidateCachedResponses() = 0; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBCursor) + + protected: + IDBCursor(indexedDB::BackgroundCursorChildBase* aBackgroundActor); + + // TODO: Check if we can remove virtual by changing cycle collection. + virtual ~IDBCursor() = default; + + void ResetBase(); +}; + +template <IDBCursor::Type CursorType> +class IDBTypedCursor : public IDBCursor { + public: + template <typename... DataArgs> + explicit IDBTypedCursor( + indexedDB::BackgroundCursorChild<CursorType>* aBackgroundActor, + DataArgs&&... aDataArgs); + + static constexpr Type GetType() { return CursorType; } + + // Checks if this is a locale aware cursor (ie. the index's sortKey is unset) + bool IsLocaleAware() const; + + void GetSource(OwningIDBObjectStoreOrIDBIndex& aSource) const final; + + void GetKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) final; + + void GetPrimaryKey(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) final; + + void GetValue(JSContext* aCx, JS::MutableHandle<JS::Value> aResult, + ErrorResult& aRv) final; + + void Continue(JSContext* aCx, JS::Handle<JS::Value> aKey, + ErrorResult& aRv) final; + + void ContinuePrimaryKey(JSContext* aCx, JS::Handle<JS::Value> aKey, + JS::Handle<JS::Value> aPrimaryKey, + ErrorResult& aRv) final; + + void Advance(uint32_t aCount, ErrorResult& aRv) final; + + [[nodiscard]] RefPtr<IDBRequest> Update(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) final; + + [[nodiscard]] RefPtr<IDBRequest> Delete(JSContext* aCx, + ErrorResult& aRv) final; + + // nsWrapperCache + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; + + void InvalidateCachedResponses() final; + + void Reset(); + + void Reset(CursorData<CursorType>&& aCursorData); + + private: + static constexpr bool IsObjectStoreCursor = + CursorTypeTraits<CursorType>::IsObjectStoreCursor; + static constexpr bool IsKeyOnlyCursor = + CursorTypeTraits<CursorType>::IsKeyOnlyCursor; + + CursorSourceType<CursorType>& GetSourceRef() const { + MOZ_ASSERT(mSource); + return *mSource; + } + + IDBObjectStore& GetSourceObjectStoreRef() const { + if constexpr (IsObjectStoreCursor) { + return GetSourceRef(); + } else { + MOZ_ASSERT(!GetSourceRef().IsDeleted()); + + auto res = GetSourceRef().ObjectStore(); + MOZ_ASSERT(res); + return *res; + } + } + + indexedDB::BackgroundCursorChild<CursorType>& GetTypedBackgroundActorRef() + const { + // We can safely downcast to BackgroundCursorChild<CursorType>*, since we + // initialized that in the constructor from that type. We just want to avoid + // having a second typed field. + return *static_cast<indexedDB::BackgroundCursorChild<CursorType>*>( + mBackgroundActor->get()); + } + + bool IsSourceDeleted() const; + + protected: + virtual ~IDBTypedCursor() override; + + void DropJSObjects(); + + CursorData<CursorType> mData; + + // TODO: mSource could be made const if Bug 1575173 is resolved. It is + // initialized in the constructor and never modified/cleared. + RefPtr<CursorSourceType<CursorType>> mSource; +}; + +// The subclasses defined by this macro are only needed to be able to use the +// cycle collector macros, which do not support templates. If spelled out, the +// cycle collection could be implemented directly on IDBTypedCursor, and these +// classes were not needed. +#define CONCRETE_IDBCURSOR_SUBCLASS(_subclassName, _cursorType) \ + class _subclassName final : public IDBTypedCursor<_cursorType> { \ + public: \ + NS_DECL_ISUPPORTS_INHERITED \ + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(_subclassName, IDBCursor) \ + \ + using IDBTypedCursor<_cursorType>::IDBTypedCursor; \ + \ + private: \ + ~_subclassName() final = default; \ + }; + +CONCRETE_IDBCURSOR_SUBCLASS(IDBObjectStoreCursor, IDBCursor::Type::ObjectStore) +CONCRETE_IDBCURSOR_SUBCLASS(IDBObjectStoreKeyCursor, + IDBCursor::Type::ObjectStoreKey) +CONCRETE_IDBCURSOR_SUBCLASS(IDBIndexCursor, IDBCursor::Type::Index) +CONCRETE_IDBCURSOR_SUBCLASS(IDBIndexKeyCursor, IDBCursor::Type::IndexKey) + +template <IDBCursor::Type CursorType> +using IDBCursorImpl = typename CursorTypeTraits<CursorType>::Type; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbcursor_h__ diff --git a/dom/indexedDB/IDBCursorType.cpp b/dom/indexedDB/IDBCursorType.cpp new file mode 100644 index 0000000000..34bb1aa8af --- /dev/null +++ b/dom/indexedDB/IDBCursorType.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 "IDBCursorType.h" + +#include "IndexedDatabaseInlines.h" + +namespace mozilla::dom { +CommonCursorDataBase::CommonCursorDataBase(Key aKey) : mKey{std::move(aKey)} {} + +IndexCursorDataBase::IndexCursorDataBase(Key aKey, Key aLocaleAwareKey, + Key aObjectStoreKey) + : CommonCursorDataBase{std::move(aKey)}, + mLocaleAwareKey{std::move(aLocaleAwareKey)}, + mObjectStoreKey{std::move(aObjectStoreKey)} {} + +ValueCursorDataBase::ValueCursorDataBase( + StructuredCloneReadInfoChild&& aCloneInfo) + : mCloneInfo{std::move(aCloneInfo)} {} + +CursorData<IDBCursorType::ObjectStore>::CursorData( + Key aKey, StructuredCloneReadInfoChild&& aCloneInfo) + : ObjectStoreCursorDataBase{std::move(aKey)}, + ValueCursorDataBase{std::move(aCloneInfo)} {} + +CursorData<IDBCursorType::Index>::CursorData( + Key aKey, Key aLocaleAwareKey, Key aObjectStoreKey, + StructuredCloneReadInfoChild&& aCloneInfo) + : IndexCursorDataBase{std::move(aKey), std::move(aLocaleAwareKey), + std::move(aObjectStoreKey)}, + ValueCursorDataBase{std::move(aCloneInfo)} {} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBCursorType.h b/dom/indexedDB/IDBCursorType.h new file mode 100644 index 0000000000..79ecd2881a --- /dev/null +++ b/dom/indexedDB/IDBCursorType.h @@ -0,0 +1,152 @@ +/* -*- 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_idbcursortype_h__ +#define mozilla_dom_idbcursortype_h__ + +#include "IndexedDatabase.h" +#include "mozilla/dom/indexedDB/Key.h" + +namespace mozilla::dom { +namespace indexedDB { +class ObjectStoreCursorResponse; +class ObjectStoreKeyCursorResponse; +class IndexCursorResponse; +class IndexKeyCursorResponse; +} // namespace indexedDB + +enum struct IDBCursorType { + ObjectStore, + ObjectStoreKey, + Index, + IndexKey, +}; + +template <IDBCursorType CursorType> +struct CursorTypeTraits; + +class IDBIndex; +class IDBObjectStore; + +class IDBIndexCursor; +class IDBIndexKeyCursor; +class IDBObjectStoreCursor; +class IDBObjectStoreKeyCursor; + +template <> +struct CursorTypeTraits<IDBCursorType::Index> { + using Type = IDBIndexCursor; + using ResponseType = indexedDB::IndexCursorResponse; + static constexpr bool IsObjectStoreCursor = false; + static constexpr bool IsKeyOnlyCursor = false; +}; + +template <> +struct CursorTypeTraits<IDBCursorType::IndexKey> { + using Type = IDBIndexKeyCursor; + using ResponseType = indexedDB::IndexKeyCursorResponse; + static constexpr bool IsObjectStoreCursor = false; + static constexpr bool IsKeyOnlyCursor = true; +}; + +template <> +struct CursorTypeTraits<IDBCursorType::ObjectStore> { + using Type = IDBObjectStoreCursor; + using ResponseType = indexedDB::ObjectStoreCursorResponse; + static constexpr bool IsObjectStoreCursor = true; + static constexpr bool IsKeyOnlyCursor = false; +}; + +template <> +struct CursorTypeTraits<IDBCursorType::ObjectStoreKey> { + using Type = IDBObjectStoreKeyCursor; + using ResponseType = indexedDB::ObjectStoreKeyCursorResponse; + static constexpr bool IsObjectStoreCursor = true; + static constexpr bool IsKeyOnlyCursor = true; +}; + +template <IDBCursorType CursorType> +using CursorSourceType = + std::conditional_t<CursorTypeTraits<CursorType>::IsObjectStoreCursor, + IDBObjectStore, IDBIndex>; + +using Key = indexedDB::Key; +using StructuredCloneReadInfoChild = indexedDB::StructuredCloneReadInfoChild; + +struct CommonCursorDataBase { + CommonCursorDataBase() = delete; + + explicit CommonCursorDataBase(Key aKey); + + Key mKey; ///< The current key, i.e. the key representing the cursor's + ///< position + ///< (https://w3c.github.io/IndexedDB/#cursor-position). +}; + +template <IDBCursorType CursorType> +struct CursorData; + +struct ObjectStoreCursorDataBase : CommonCursorDataBase { + using CommonCursorDataBase::CommonCursorDataBase; + + const Key& GetSortKey(const bool aIsLocaleAware) const { + MOZ_ASSERT(!aIsLocaleAware); + return GetObjectStoreKey(); + } + const Key& GetObjectStoreKey() const { return mKey; } + static constexpr const char* GetObjectStoreKeyForLogging() { return "NA"; } +}; + +struct IndexCursorDataBase : CommonCursorDataBase { + IndexCursorDataBase(Key aKey, Key aLocaleAwareKey, Key aObjectStoreKey); + + const Key& GetSortKey(const bool aIsLocaleAware) const { + return aIsLocaleAware ? mLocaleAwareKey : mKey; + } + const Key& GetObjectStoreKey() const { return mObjectStoreKey; } + const char* GetObjectStoreKeyForLogging() const { + return GetObjectStoreKey().GetBuffer().get(); + } + + Key mLocaleAwareKey; ///< If the index's mLocale is set, this is mKey + ///< converted to mLocale. Otherwise, it is unset. + Key mObjectStoreKey; ///< The key representing the cursor's object store + ///< position + ///< (https://w3c.github.io/IndexedDB/#cursor-object-store-position). +}; + +struct ValueCursorDataBase { + explicit ValueCursorDataBase(StructuredCloneReadInfoChild&& aCloneInfo); + + StructuredCloneReadInfoChild mCloneInfo; +}; + +template <> +struct CursorData<IDBCursorType::ObjectStoreKey> : ObjectStoreCursorDataBase { + using ObjectStoreCursorDataBase::ObjectStoreCursorDataBase; +}; + +template <> +struct CursorData<IDBCursorType::ObjectStore> : ObjectStoreCursorDataBase, + ValueCursorDataBase { + CursorData(Key aKey, StructuredCloneReadInfoChild&& aCloneInfo); +}; + +template <> +struct CursorData<IDBCursorType::IndexKey> : IndexCursorDataBase { + using IndexCursorDataBase::IndexCursorDataBase; +}; + +template <> +struct CursorData<IDBCursorType::Index> : IndexCursorDataBase, + ValueCursorDataBase { + CursorData(Key aKey, Key aLocaleAwareKey, Key aObjectStoreKey, + StructuredCloneReadInfoChild&& aCloneInfo); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/indexedDB/IDBDatabase.cpp b/dom/indexedDB/IDBDatabase.cpp new file mode 100644 index 0000000000..455ea2bb69 --- /dev/null +++ b/dom/indexedDB/IDBDatabase.cpp @@ -0,0 +1,1078 @@ +/* -*- 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 "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBObjectStore.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDatabaseManager.h" +#include "IndexedDBCommon.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventDispatcher.h" +#include "MainThreadUtils.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Services.h" +#include "mozilla/storage.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/BindingDeclarations.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/dom/IPCBlobUtils.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/FileDescriptor.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Document.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsISupportsPrimitives.h" +#include "nsThreadUtils.h" +#include "nsIWeakReferenceUtils.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::dom { + +using namespace mozilla::dom::indexedDB; +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(nsCOMPtr<nsIRunnable> aRunnable) + : CancelableRunnable("dom::CancelableRunnableWrapper"), + mRunnable(std::move(aRunnable)) { + MOZ_ASSERT(mRunnable); + } + + private: + ~CancelableRunnableWrapper() = default; + + 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, + SafeRefPtr<IDBFactory> aFactory, + BackgroundDatabaseChild* aActor, + UniquePtr<DatabaseSpec> aSpec) + : DOMEventTargetHelper(aRequest), + mFactory(std::move(aFactory)), + mSpec(std::move(aSpec)), + mBackgroundActor(aActor), + mClosed(false), + mInvalidated(false), + mQuotaExceeded(false), + mIncreasedActiveDatabaseCount(false) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(mFactory); + mFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(mSpec); +} + +IDBDatabase::~IDBDatabase() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mBackgroundActor); + MOZ_ASSERT(!mIncreasedActiveDatabaseCount); +} + +// static +RefPtr<IDBDatabase> IDBDatabase::Create(IDBOpenDBRequest* aRequest, + SafeRefPtr<IDBFactory> aFactory, + BackgroundDatabaseChild* aActor, + UniquePtr<DatabaseSpec> aSpec) { + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aSpec); + + RefPtr<IDBDatabase> db = + new IDBDatabase(aRequest, aFactory.clonePtr(), aActor, std::move(aSpec)); + + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aFactory->GetParentObject()); + if (window) { + 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. + MOZ_ALWAYS_SUCCEEDS( + obsSvc->AddObserver(observer, kWindowObserverTopic, false)); + + // These topics are not crucial. + QM_WARNONLY_TRY(QM_TO_RESULT( + obsSvc->AddObserver(observer, kCycleCollectionObserverTopic, false))); + QM_WARNONLY_TRY(QM_TO_RESULT( + obsSvc->AddObserver(observer, kMemoryPressureObserverTopic, false))); + + db->mObserver = std::move(observer); + } + } + + db->IncreaseActiveDatabaseCount(); + + return db; +} + +#ifdef DEBUG + +void IDBDatabase::AssertIsOnOwningThread() const { + MOZ_ASSERT(mFactory); + mFactory->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +nsIEventTarget* IDBDatabase::EventTarget() const { + AssertIsOnOwningThread(); + return mFactory->EventTarget(); +} + +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(); + } + + // Decrease the number of active databases right after the database is + // closed. + MaybeDecreaseActiveDatabaseCount(); + } +} + +void IDBDatabase::InvalidateInternal() { + AssertIsOnOwningThread(); + + AbortTransactions(/* aShouldWarn */ true); + + CloseInternal(); +} + +void IDBDatabase::EnterSetVersionTransaction(uint64_t aNewVersion) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aNewVersion); + MOZ_ASSERT(!RunningVersionChangeTransaction()); + MOZ_ASSERT(mSpec); + MOZ_ASSERT(!mPreviousSpec); + + mPreviousSpec = MakeUnique<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! + auto currentSpec = std::move(mSpec); + + mSpec = std::move(mPreviousSpec); + + RefreshSpec(/* aMayDelete */ true); +} + +void IDBDatabase::RefreshSpec(bool aMayDelete) { + AssertIsOnOwningThread(); + + for (auto* weakTransaction : mTransactions) { + const auto transaction = + SafeRefPtr{weakTransaction, AcquireStrongRefFromRawPtr{}}; + MOZ_ASSERT(transaction); + transaction->AssertIsOnOwningThread(); + transaction->RefreshSpec(aMayDelete); + } +} + +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(); +} + +RefPtr<DOMStringList> IDBDatabase::ObjectStoreNames() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mSpec); + + return CreateSortedDOMStringList( + mSpec->objectStores(), + [](const auto& objectStore) { return objectStore.metadata().name(); }); +} + +RefPtr<Document> IDBDatabase::GetOwnerDocument() const { + if (nsPIDOMWindowInner* window = GetOwner()) { + return window->GetExtantDoc(); + } + return nullptr; +} + +RefPtr<IDBObjectStore> IDBDatabase::CreateObjectStore( + const nsAString& aName, const IDBObjectStoreParameters& aOptionalParameters, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + const auto transaction = IDBTransaction::MaybeCurrent(); + if (!transaction || transaction->Database() != this || + transaction->GetMode() != IDBTransaction::Mode::VersionChange) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!transaction->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + QM_INFOONLY_TRY_UNWRAP(const auto maybeKeyPath, + KeyPath::Parse(aOptionalParameters.mKeyPath)); + if (!maybeKeyPath) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + + const auto& keyPath = maybeKeyPath.ref(); + + auto& objectStores = mSpec->objectStores(); + const auto end = objectStores.cend(); + const auto foundIt = std::find_if( + objectStores.cbegin(), end, [&aName](const auto& objectStore) { + return aName == objectStore.metadata().name(); + }); + if (foundIt != end) { + aRv.ThrowConstraintError(nsPrintfCString( + "Object store named '%s' already exists at index '%zu'", + NS_ConvertUTF16toUTF8(aName).get(), foundIt.GetIndex())); + 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); + } + + auto 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).createObjectStore(%s)", + "IDBDatabase.createObjectStore(%.0s%.0s%.0s)", + transaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(*transaction), + IDB_LOG_STRINGIFY(objectStore)); + + return objectStore; +} + +void IDBDatabase::DeleteObjectStore(const nsAString& aName, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + const auto transaction = IDBTransaction::MaybeCurrent(); + if (!transaction || transaction->Database() != this || + transaction->GetMode() != IDBTransaction::Mode::VersionChange) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + if (!transaction->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + auto& specArray = mSpec->objectStores(); + const auto end = specArray.end(); + const auto foundIt = + std::find_if(specArray.begin(), end, [&aName](const auto& objectStore) { + const ObjectStoreMetadata& metadata = objectStore.metadata(); + MOZ_ASSERT(metadata.id()); + + return aName == metadata.name(); + }); + + if (foundIt == end) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return; + } + + // Must do this before altering the metadata array! + transaction->DeleteObjectStore(foundIt->metadata().id()); + + specArray.RemoveElementAt(foundIt); + RefreshSpec(/* aMayDelete */ false); + + // 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).deleteObjectStore(\"%s\")", + "IDBDatabase.deleteObjectStore(%.0s%.0s%.0s)", + transaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(this), IDB_LOG_STRINGIFY(*transaction), + NS_ConvertUTF16toUTF8(aName).get()); +} + +RefPtr<IDBTransaction> IDBDatabase::Transaction( + JSContext* aCx, const StringOrStringSequence& aStoreNames, + IDBTransactionMode aMode, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if ((aMode == IDBTransactionMode::Readwriteflush || + aMode == IDBTransactionMode::Cleanup) && + !StaticPrefs::dom_indexedDB_experimental()) { + // 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>("argument 2", "readwriteflush", + "IDBTransactionMode"); + return nullptr; + } + + if (QuotaManager::IsShuttingDown()) { + IDB_REPORT_INTERNAL_ERR(); + aRv.ThrowUnknownError("Can't start IndexedDB transaction during shutdown"); + return nullptr; + } + + // https://w3c.github.io/IndexedDB/#dom-idbdatabase-transaction + // Step 1. + if (RunningVersionChangeTransaction()) { + aRv.ThrowInvalidStateError( + "Can't start a transaction while running an upgrade transaction"); + return nullptr; + } + + // Step 2. + if (mClosed) { + aRv.ThrowInvalidStateError( + "Can't start a transaction on a closed database"); + return nullptr; + } + + // Step 3. + AutoTArray<nsString, 1> stackSequence; + + if (aStoreNames.IsString()) { + stackSequence.AppendElement(aStoreNames.GetAsString()); + } else { + MOZ_ASSERT(aStoreNames.IsStringSequence()); + // Step 5, but it can be done before step 4 because those steps + // can't both throw. + if (aStoreNames.GetAsStringSequence().IsEmpty()) { + aRv.ThrowInvalidAccessError("Empty scope passed in"); + return nullptr; + } + } + + // Step 4. + 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); + + // While collecting object store names, check if the corresponding object + // stores actually exist. + const auto begin = objectStores.cbegin(); + const auto end = objectStores.cend(); + for (const auto& name : storeNames) { + const auto foundIt = + std::find_if(begin, end, [&name](const auto& objectStore) { + return objectStore.metadata().name() == name; + }); + if (foundIt == end) { + Telemetry::ScalarAdd( + storeNames.IsEmpty() + ? Telemetry::ScalarID:: + IDB_FAILURE_UNKNOWN_OBJECTSTORE_EMPTY_DATABASE + : Telemetry::ScalarID:: + IDB_FAILURE_UNKNOWN_OBJECTSTORE_NON_EMPTY_DATABASE, + 1); + + // Not using nsPrintfCString in case "name" has embedded nulls. + aRv.ThrowNotFoundError("'"_ns + NS_ConvertUTF16toUTF8(name) + + "' is not a known object store name"_ns); + return nullptr; + } + + sortedStoreNames.EmplaceBack(name); + } + sortedStoreNames.Sort(); + + // Remove any duplicates. + sortedStoreNames.SetLength( + std::unique(sortedStoreNames.begin(), sortedStoreNames.end()).GetIndex()); + + IDBTransaction::Mode mode; + switch (aMode) { + case IDBTransactionMode::Readonly: + mode = IDBTransaction::Mode::ReadOnly; + break; + case IDBTransactionMode::Readwrite: + if (mQuotaExceeded) { + mode = IDBTransaction::Mode::Cleanup; + mQuotaExceeded = false; + } else { + mode = IDBTransaction::Mode::ReadWrite; + } + break; + case IDBTransactionMode::Readwriteflush: + mode = IDBTransaction::Mode::ReadWriteFlush; + break; + case IDBTransactionMode::Cleanup: + mode = IDBTransaction::Mode::Cleanup; + mQuotaExceeded = false; + break; + case IDBTransactionMode::Versionchange: + // Step 6. + aRv.ThrowTypeError("Invalid transaction mode"); + return nullptr; + + default: + MOZ_CRASH("Unknown mode!"); + } + + SafeRefPtr<IDBTransaction> transaction = + IDBTransaction::Create(aCx, this, sortedStoreNames, mode); + if (NS_WARN_IF(!transaction)) { + IDB_REPORT_INTERNAL_ERR(); + MOZ_ASSERT(!NS_IsMainThread(), + "Transaction creation can only fail on workers"); + aRv.ThrowUnknownError("Failed to create IndexedDB transaction on worker"); + return nullptr; + } + + BackgroundTransactionChild* actor = + new BackgroundTransactionChild(transaction.clonePtr()); + + IDB_LOG_MARK_CHILD_TRANSACTION( + "database(%s).transaction(%s)", "IDBDatabase.transaction(%.0s%.0s)", + transaction->LoggingSerialNumber(), IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(*transaction)); + + MOZ_ALWAYS_TRUE(mBackgroundActor->SendPBackgroundIDBTransactionConstructor( + actor, sortedStoreNames, mode)); + + transaction->SetBackgroundActor(actor); + + if (mode == IDBTransaction::Mode::Cleanup) { + ExpireFileActors(/* aExpireAll */ true); + } + + return AsRefPtr(std::move(transaction)); +} + +void IDBDatabase::RegisterTransaction(IDBTransaction& aTransaction) { + AssertIsOnOwningThread(); + aTransaction.AssertIsOnOwningThread(); + MOZ_ASSERT(!mTransactions.Contains(&aTransaction)); + + mTransactions.Insert(&aTransaction); +} + +void IDBDatabase::UnregisterTransaction(IDBTransaction& aTransaction) { + AssertIsOnOwningThread(); + aTransaction.AssertIsOnOwningThread(); + MOZ_ASSERT(mTransactions.Contains(&aTransaction)); + + mTransactions.Remove(&aTransaction); +} + +void IDBDatabase::AbortTransactions(bool aShouldWarn) { + AssertIsOnOwningThread(); + + constexpr size_t StackExceptionLimit = 20; + using StrongTransactionArray = + AutoTArray<SafeRefPtr<IDBTransaction>, StackExceptionLimit>; + using WeakTransactionArray = AutoTArray<IDBTransaction*, StackExceptionLimit>; + + if (!mTransactions.Count()) { + // Return early as an optimization, the remainder is a no-op in this + // case. + return; + } + + // XXX TransformIfIntoNewArray might be generalized to allow specifying the + // type of nsTArray to create, so that it can create an AutoTArray as well; an + // TransformIf (without AbortOnErr) might be added, which could be used here. + StrongTransactionArray transactionsToAbort; + transactionsToAbort.SetCapacity(mTransactions.Count()); + + for (IDBTransaction* const transaction : mTransactions) { + 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->IsFinished()) { + transactionsToAbort.EmplaceBack(transaction, + AcquireStrongRefFromRawPtr{}); + } + } + MOZ_ASSERT(transactionsToAbort.Length() <= mTransactions.Count()); + + if (transactionsToAbort.IsEmpty()) { + // Return early as an optimization, the remainder is a no-op in this + // case. + 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 (const auto& transaction : transactionsToAbort) { + MOZ_ASSERT(transaction); + MOZ_ASSERT(!transaction->IsFinished()); + + // We warn for any transactions that could have written data, but + // ignore read-only transactions. + if (aShouldWarn && transaction->IsWriteAllowed()) { + transactionsThatNeedWarning.AppendElement(transaction.unsafeGetRawPtr()); + } + + 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); + + LogWarning(kWarningMessage, filename, lineNo, column); + } +} + +PBackgroundIDBDatabaseFileChild* IDBDatabase::GetOrCreateFileActorForBlob( + Blob& aBlob) { + AssertIsOnOwningThread(); + 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. + nsWeakPtr weakRef = do_GetWeakReference(&aBlob); + MOZ_ASSERT(weakRef); + + PBackgroundIDBDatabaseFileChild* actor = nullptr; + + if (!mFileActors.Get(weakRef, &actor)) { + BlobImpl* blobImpl = aBlob.Impl(); + MOZ_ASSERT(blobImpl); + + IPCBlob ipcBlob; + nsresult rv = IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + auto* dbFile = new DatabaseFile(this); + + actor = mBackgroundActor->SendPBackgroundIDBDatabaseFileConstructor( + dbFile, ipcBlob); + if (NS_WARN_IF(!actor)) { + return nullptr; + } + + mFileActors.InsertOrUpdate(weakRef, actor); + } + + MOZ_ASSERT(actor); + + return actor; +} + +void IDBDatabase::NoteFinishedFileActor( + PBackgroundIDBDatabaseFileChild* aFileActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aFileActor); + + mFileActors.RemoveIf([aFileActor](const auto& iter) { + MOZ_ASSERT(iter.Key()); + PBackgroundIDBDatabaseFileChild* actor = iter.Data(); + MOZ_ASSERT(actor); + + return actor == aFileActor; + }); +} + +void IDBDatabase::NoteActiveTransaction() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mFactory); + + // Increase the number of active transactions. + mFactory->UpdateActiveTransactionCount(1); +} + +void IDBDatabase::NoteInactiveTransaction() { + AssertIsOnOwningThread(); + + if (!mBackgroundActor || !mFileActors.Count()) { + MOZ_ASSERT(mFactory); + mFactory->UpdateActiveTransactionCount(-1); + return; + } + + RefPtr<Runnable> runnable = + NewRunnableMethod("IDBDatabase::NoteInactiveTransactionDelayed", this, + &IDBDatabase::NoteInactiveTransactionDelayed); + MOZ_ASSERT(runnable); + + if (!NS_IsMainThread()) { + // Wrap as a nsICancelableRunnable to make workers happy. + runnable = MakeRefPtr<CancelableRunnableWrapper>(runnable.forget()); + } + + MOZ_ALWAYS_SUCCEEDS( + EventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); +} + +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) { + nsWeakPtr 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()); + } +} + +void IDBDatabase::Invalidate() { + AssertIsOnOwningThread(); + + if (!mInvalidated) { + mInvalidated = true; + + InvalidateInternal(); + } +} + +void IDBDatabase::NoteInactiveTransactionDelayed() { + ExpireFileActors(/* aExpireAll */ false); + + MOZ_ASSERT(mFactory); + mFactory->UpdateActiveTransactionCount(-1); +} + +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, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(IDBDatabase, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBDatabase) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBDatabase) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBDatabase, + DOMEventTargetHelper) + tmp->AssertIsOnOwningThread(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFactory) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(IDBDatabase, + DOMEventTargetHelper) + 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::DisconnectFromOwner() { + InvalidateInternal(); + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void IDBDatabase::LastRelease() { + AssertIsOnOwningThread(); + + CloseInternal(); + + ExpireFileActors(/* aExpireAll */ true); + + if (mBackgroundActor) { + mBackgroundActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } +} + +JSObject* IDBDatabase::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return IDBDatabase_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMETHODIMP +CancelableRunnableWrapper::Run() { + const nsCOMPtr<nsIRunnable> runnable = std::move(mRunnable); + + 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 (auto& objSpec : objectStores) { + const bool idIsCurrent = objSpec.metadata().id() == aObjectStoreId; + + if (idIsCurrent) { + MOZ_ASSERT(!foundObjectStoreSpec); + foundObjectStoreSpec = &objSpec; + } + + if (objSpec.metadata().name() == aName) { + if (idIsCurrent) { + return NS_OK; + } + return NS_ERROR_DOM_INDEXEDDB_RENAME_OBJECT_STORE_ERR; + } + } + + MOZ_ASSERT(foundObjectStoreSpec); + + // Update the name of the matched object store. + foundObjectStoreSpec->metadata().name().Assign(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_RENAME_INDEX_ERR; + } + } + + MOZ_ASSERT(foundIndexMetadata); + + // Update the name of the matched object store. + foundIndexMetadata->name() = nsString(aName); + + return NS_OK; +} + +void IDBDatabase::IncreaseActiveDatabaseCount() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mFactory); + MOZ_ASSERT(!mIncreasedActiveDatabaseCount); + + mFactory->UpdateActiveDatabaseCount(1); + mIncreasedActiveDatabaseCount = true; +} + +void IDBDatabase::MaybeDecreaseActiveDatabaseCount() { + AssertIsOnOwningThread(); + + if (mIncreasedActiveDatabaseCount) { + // Decrease the number of active databases. + MOZ_ASSERT(mFactory); + mFactory->UpdateActiveDatabaseCount(-1); + mIncreasedActiveDatabaseCount = false; + } +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBDatabase.h b/dom/indexedDB/IDBDatabase.h new file mode 100644 index 0000000000..3f7c4aa63c --- /dev/null +++ b/dom/indexedDB/IDBDatabase.h @@ -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/. */ + +#ifndef mozilla_dom_idbdatabase_h__ +#define mozilla_dom_idbdatabase_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBTransactionBinding.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/UniquePtr.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "nsString.h" +#include "nsTHashSet.h" + +class nsIEventTarget; +class nsIGlobalObject; + +namespace mozilla { + +class ErrorResult; +class EventChainPostVisitor; + +namespace dom { + +class Blob; +class DOMStringList; +class IDBFactory; +class IDBObjectStore; +struct IDBObjectStoreParameters; +class IDBOpenDBRequest; +class IDBRequest; +class IDBTransaction; +template <class> +class Optional; +class StringOrStringSequence; + +namespace indexedDB { +class BackgroundDatabaseChild; +class PBackgroundIDBDatabaseFileChild; +} // namespace indexedDB + +class IDBDatabase final : public DOMEventTargetHelper { + using DatabaseSpec = mozilla::dom::indexedDB::DatabaseSpec; + using PersistenceType = mozilla::dom::quota::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. + SafeRefPtr<IDBFactory> mFactory; + + UniquePtr<DatabaseSpec> mSpec; + + // Normally null except during a versionchange transaction. + UniquePtr<DatabaseSpec> mPreviousSpec; + + indexedDB::BackgroundDatabaseChild* mBackgroundActor; + + nsTHashSet<IDBTransaction*> mTransactions; + + nsTHashMap<nsISupportsHashKey, indexedDB::PBackgroundIDBDatabaseFileChild*> + mFileActors; + + RefPtr<Observer> mObserver; + + bool mClosed; + bool mInvalidated; + bool mQuotaExceeded; + bool mIncreasedActiveDatabaseCount; + + public: + [[nodiscard]] static RefPtr<IDBDatabase> Create( + IDBOpenDBRequest* aRequest, SafeRefPtr<IDBFactory> aFactory, + indexedDB::BackgroundDatabaseChild* aActor, + UniquePtr<DatabaseSpec> aSpec); + + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + nsIEventTarget* EventTarget() const; + + const nsString& Name() const; + + void GetName(nsAString& aName) const { + AssertIsOnOwningThread(); + + aName = Name(); + } + + uint64_t Version() const; + + [[nodiscard]] RefPtr<Document> 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(); + + void RegisterTransaction(IDBTransaction& aTransaction); + + void UnregisterTransaction(IDBTransaction& aTransaction); + + void AbortTransactions(bool aShouldWarn); + + indexedDB::PBackgroundIDBDatabaseFileChild* GetOrCreateFileActorForBlob( + Blob& aBlob); + + void NoteFinishedFileActor( + indexedDB::PBackgroundIDBDatabaseFileChild* aFileActor); + + void NoteActiveTransaction(); + + void NoteInactiveTransaction(); + + [[nodiscard]] RefPtr<DOMStringList> ObjectStoreNames() const; + + [[nodiscard]] RefPtr<IDBObjectStore> CreateObjectStore( + const nsAString& aName, + const IDBObjectStoreParameters& aOptionalParameters, ErrorResult& aRv); + + void DeleteObjectStore(const nsAString& name, ErrorResult& aRv); + + // This will be called from the DOM. + [[nodiscard]] RefPtr<IDBTransaction> Transaction( + JSContext* aCx, const StringOrStringSequence& aStoreNames, + IDBTransactionMode aMode, ErrorResult& aRv); + + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(close) + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(versionchange) + + void ClearBackgroundActor() { + AssertIsOnOwningThread(); + + // Decrease the number of active databases if it was not done in + // CloseInternal(). + MaybeDecreaseActiveDatabaseCount(); + + mBackgroundActor = nullptr; + } + + const DatabaseSpec* Spec() const { return mSpec.get(); } + + template <typename Pred> + indexedDB::ObjectStoreSpec* LookupModifiableObjectStoreSpec(Pred&& aPred) { + auto& objectStores = mSpec->objectStores(); + const auto foundIt = + std::find_if(objectStores.begin(), objectStores.end(), aPred); + return foundIt != objectStores.end() ? &*foundIt : nullptr; + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBDatabase, DOMEventTargetHelper) + + // DOMEventTargetHelper + void DisconnectFromOwner() override; + + virtual void LastRelease() override; + + // nsWrapperCache + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + IDBDatabase(IDBOpenDBRequest* aRequest, SafeRefPtr<IDBFactory> aFactory, + indexedDB::BackgroundDatabaseChild* aActor, + UniquePtr<DatabaseSpec> aSpec); + + ~IDBDatabase(); + + void CloseInternal(); + + void InvalidateInternal(); + + bool RunningVersionChangeTransaction() const { + AssertIsOnOwningThread(); + + return !!mPreviousSpec; + } + + void RefreshSpec(bool aMayDelete); + + void ExpireFileActors(bool aExpireAll); + + void NoteInactiveTransactionDelayed(); + + 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); + + void IncreaseActiveDatabaseCount(); + + void MaybeDecreaseActiveDatabaseCount(); +}; + +} // 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 0000000000..75485cd225 --- /dev/null +++ b/dom/indexedDB/IDBEvents.cpp @@ -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/. */ + +#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::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"; + +RefPtr<Event> CreateGenericEvent(EventTarget* aOwner, + const nsDependentString& aType, + Bubbles aBubbles, Cancelable aCancelable) { + RefPtr<Event> event = MakeAndAddRef<Event>(aOwner, nullptr, nullptr); + + event->InitEvent(aType, aBubbles == eDoesBubble, aCancelable == eCancelable); + + event->SetTrusted(true); + + return event; +} + +} // namespace indexedDB + +// static +RefPtr<IDBVersionChangeEvent> IDBVersionChangeEvent::CreateInternal( + EventTarget* aOwner, const nsAString& aType, uint64_t aOldVersion, + const 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; +} + +RefPtr<IDBVersionChangeEvent> IDBVersionChangeEvent::Create( + EventTarget* aOwner, const nsDependentString& aName, uint64_t aOldVersion, + uint64_t aNewVersion) { + Nullable<uint64_t> newVersion(aNewVersion); + return CreateInternal(aOwner, aName, aOldVersion, newVersion); +} + +RefPtr<IDBVersionChangeEvent> IDBVersionChangeEvent::Create( + EventTarget* aOwner, const nsDependentString& aName, uint64_t aOldVersion) { + Nullable<uint64_t> newVersion(0); + newVersion.SetNull(); + return CreateInternal(aOwner, aName, aOldVersion, newVersion); +} + +RefPtr<IDBVersionChangeEvent> IDBVersionChangeEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const IDBVersionChangeEventInit& aOptions) { + 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 IDBVersionChangeEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBEvents.h b/dom/indexedDB/IDBEvents.h new file mode 100644 index 0000000000..2b27261404 --- /dev/null +++ b/dom/indexedDB/IDBEvents.h @@ -0,0 +1,99 @@ +/* -*- 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" +#include "nsStringFwd.h" + +#define IDBVERSIONCHANGEEVENT_IID \ + { \ + 0x3b65d4c3, 0x73ad, 0x492e, { \ + 0xb1, 0x2d, 0x15, 0xf9, 0xda, 0xc2, 0x08, 0x4b \ + } \ + } + +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; + +[[nodiscard]] RefPtr<Event> 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: + [[nodiscard]] static RefPtr<IDBVersionChangeEvent> Create( + EventTarget* aOwner, const nsDependentString& aName, uint64_t aOldVersion, + uint64_t aNewVersion); + + [[nodiscard]] static RefPtr<IDBVersionChangeEvent> Create( + EventTarget* aOwner, const nsDependentString& aName, + uint64_t aOldVersion); + + [[nodiscard]] static RefPtr<IDBVersionChangeEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const IDBVersionChangeEventInit& aOptions); + + uint64_t OldVersion() const { return mOldVersion; } + + Nullable<uint64_t> GetNewVersion() const { return mNewVersion; } + + NS_DECLARE_STATIC_IID_ACCESSOR(IDBVERSIONCHANGEEVENT_IID) + + NS_DECL_ISUPPORTS_INHERITED + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + private: + IDBVersionChangeEvent(EventTarget* aOwner, uint64_t aOldVersion) + : Event(aOwner, nullptr, nullptr), mOldVersion(aOldVersion) {} + + ~IDBVersionChangeEvent() = default; + + [[nodiscard]] static RefPtr<IDBVersionChangeEvent> CreateInternal( + EventTarget* aOwner, const nsAString& aType, uint64_t aOldVersion, + const 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 0000000000..9bc3864c30 --- /dev/null +++ b/dom/indexedDB/IDBFactory.cpp @@ -0,0 +1,780 @@ +/* -*- 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/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/IDBFactoryBinding.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackground.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/Telemetry.h" +#include "nsAboutProtocolUtils.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsIAboutModule.h" +#include "nsILoadContext.h" +#include "nsIURI.h" +#include "nsIUUIDGenerator.h" +#include "nsIWebNavigation.h" +#include "nsNetUtil.h" +#include "nsSandboxFlags.h" +#include "nsServiceManagerUtils.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "ThreadLocal.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla::dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; + +namespace { + +Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT IdentifyPrincipalType( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + switch (aPrincipalInfo.type()) { + case PrincipalInfo::TSystemPrincipalInfo: + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT::system; + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + nsCOMPtr<nsIURI> uri; + + if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(uri), info.spec())))) { + // This could be discriminated as an extra error value, but this is + // extremely unlikely to fail, so we just misuse ContentOther + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT:: + content_other; + } + + // TODO Are there constants defined for the schemes somewhere? + if (uri->SchemeIs("file")) { + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT:: + content_file; + } + if (uri->SchemeIs("http") || uri->SchemeIs("https")) { + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT:: + content_http_https; + } + if (uri->SchemeIs("moz-extension")) { + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT:: + content_moz_ext; + } + if (uri->SchemeIs("about")) { + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT:: + content_about; + } + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT:: + content_other; + } + case PrincipalInfo::TExpandedPrincipalInfo: + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT::expanded; + default: + return Telemetry::LABELS_IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT::other; + } +} + +} // namespace + +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(const IDBFactoryGuard&) + : mBackgroundActor(nullptr), + mInnerWindowID(0), + mActiveTransactionCount(0), + mActiveDatabaseCount(0), + mBackgroundActorFailed(false), + mPrivateBrowsingMode(false) { + AssertIsOnOwningThread(); +} + +IDBFactory::~IDBFactory() { + MOZ_ASSERT_IF(mBackgroundActorFailed, !mBackgroundActor); + + if (mBackgroundActor) { + mBackgroundActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mBackgroundActor, "SendDeleteMeInternal should have cleared!"); + } +} + +// static +Result<RefPtr<IDBFactory>, nsresult> IDBFactory::CreateForWindow( + nsPIDOMWindowInner* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = AllowedForWindowInternal(aWindow, &principal); + + if (rv == NS_ERROR_DOM_NOT_SUPPORTED_ERR) { + NS_WARNING("IndexedDB is not permitted in a third-party window."); + return RefPtr<IDBFactory>{}; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + if (rv == NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR) { + IDB_REPORT_INTERNAL_ERR(); + } + return Err(rv); + } + + MOZ_ASSERT(principal); + + auto principalInfo = MakeUnique<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(principal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo || + principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); + + if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(*principalInfo))) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(aWindow); + nsCOMPtr<nsILoadContext> loadContext = do_QueryInterface(webNav); + + auto factory = MakeRefPtr<IDBFactory>(IDBFactoryGuard{}); + factory->mPrincipalInfo = std::move(principalInfo); + + factory->mGlobal = do_QueryInterface(aWindow); + MOZ_ASSERT(factory->mGlobal); + + factory->mBrowserChild = BrowserChild::GetFrom(aWindow); + factory->mEventTarget = + nsGlobalWindowInner::Cast(aWindow)->EventTargetFor(TaskCategory::Other); + factory->mInnerWindowID = aWindow->WindowID(); + factory->mPrivateBrowsingMode = + loadContext && loadContext->UsePrivateBrowsing(); + + return factory; +} + +// static +Result<RefPtr<IDBFactory>, nsresult> IDBFactory::CreateForMainThreadJS( + nsIGlobalObject* aGlobal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aGlobal); + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal); + if (NS_WARN_IF(!sop)) { + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + auto principalInfo = MakeUnique<PrincipalInfo>(); + nsIPrincipal* principal = sop->GetEffectiveStoragePrincipal(); + MOZ_ASSERT(principal); + bool isSystem; + if (!AllowedForPrincipal(principal, &isSystem)) { + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + nsresult rv = PrincipalToPrincipalInfo(principal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(*principalInfo))) { + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + return CreateForMainThreadJSInternal(aGlobal, std::move(principalInfo)); +} + +// static +Result<RefPtr<IDBFactory>, nsresult> IDBFactory::CreateForWorker( + nsIGlobalObject* aGlobal, const PrincipalInfo& aPrincipalInfo, + uint64_t aInnerWindowID) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aPrincipalInfo.type() != PrincipalInfo::T__None); + + return CreateInternal(aGlobal, MakeUnique<PrincipalInfo>(aPrincipalInfo), + aInnerWindowID); +} + +// static +Result<RefPtr<IDBFactory>, nsresult> IDBFactory::CreateForMainThreadJSInternal( + nsIGlobalObject* aGlobal, UniquePtr<PrincipalInfo> aPrincipalInfo) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aPrincipalInfo); + + IndexedDatabaseManager* mgr = IndexedDatabaseManager::GetOrCreate(); + if (NS_WARN_IF(!mgr)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + return CreateInternal(aGlobal, std::move(aPrincipalInfo), + /* aInnerWindowID */ 0); +} + +// static +Result<RefPtr<IDBFactory>, nsresult> IDBFactory::CreateInternal( + nsIGlobalObject* aGlobal, UniquePtr<PrincipalInfo> aPrincipalInfo, + uint64_t aInnerWindowID) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aPrincipalInfo); + MOZ_ASSERT(aPrincipalInfo->type() != PrincipalInfo::T__None); + + if (aPrincipalInfo->type() != PrincipalInfo::TContentPrincipalInfo && + aPrincipalInfo->type() != PrincipalInfo::TSystemPrincipalInfo) { + NS_WARNING("IndexedDB not allowed for this principal!"); + return RefPtr<IDBFactory>{}; + } + + auto factory = MakeRefPtr<IDBFactory>(IDBFactoryGuard{}); + factory->mPrincipalInfo = std::move(aPrincipalInfo); + factory->mGlobal = aGlobal; + factory->mEventTarget = GetCurrentSerialEventTarget(); + factory->mInnerWindowID = aInnerWindowID; + + return factory; +} + +// static +bool IDBFactory::AllowedForWindow(nsPIDOMWindowInner* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + return !NS_WARN_IF(NS_FAILED(AllowedForWindowInternal(aWindow, nullptr))); +} + +// static +nsresult IDBFactory::AllowedForWindowInternal( + nsPIDOMWindowInner* aWindow, nsCOMPtr<nsIPrincipal>* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + if (NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate())) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + StorageAccess access = 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 == StorageAccess::eDeny) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (ShouldPartitionStorage(access) && + !StoragePartitioningEnabled( + access, aWindow->GetExtantDoc()->CookieJarSettings())) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + MOZ_ASSERT(sop); + + nsCOMPtr<nsIPrincipal> principal = sop->GetEffectiveStoragePrincipal(); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + if (principal->IsSystemPrincipal()) { + *aPrincipal = std::move(principal); + return NS_OK; + } + + // About URIs shouldn't be able to access IndexedDB unless they have the + // nsIAboutModule::ENABLE_INDEXED_DB flag set on them. + + if (principal->SchemeIs("about")) { + uint32_t flags; + if (NS_SUCCEEDED(principal->GetAboutModuleFlags(&flags))) { + if (!(flags & nsIAboutModule::ENABLE_INDEXED_DB)) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } else { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } + + if (aPrincipal) { + *aPrincipal = std::move(principal); + } + 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 (aPrincipal->IsSystemPrincipal()) { + if (aIsSystemPrincipal) { + *aIsSystemPrincipal = true; + } + return true; + } + + if (aIsSystemPrincipal) { + *aIsSystemPrincipal = false; + } + + return !aPrincipal->GetIsNullPrincipal(); +} + +void IDBFactory::UpdateActiveTransactionCount(int32_t aDelta) { + AssertIsOnOwningThread(); + MOZ_DIAGNOSTIC_ASSERT(aDelta > 0 || (mActiveTransactionCount + aDelta) < + mActiveTransactionCount); + mActiveTransactionCount += aDelta; +} + +void IDBFactory::UpdateActiveDatabaseCount(int32_t aDelta) { + AssertIsOnOwningThread(); + MOZ_DIAGNOSTIC_ASSERT(aDelta > 0 || + (mActiveDatabaseCount + aDelta) < mActiveDatabaseCount); + mActiveDatabaseCount += aDelta; + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (window) { + window->UpdateActiveIndexedDBDatabaseCount(aDelta); + } +} + +bool IDBFactory::IsChrome() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mPrincipalInfo); + + return mPrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo; +} + +RefPtr<IDBOpenDBRequest> IDBFactory::Open(JSContext* aCx, + const nsAString& aName, + uint64_t aVersion, + CallerType aCallerType, + ErrorResult& aRv) { + return OpenInternal(aCx, + /* aPrincipal */ nullptr, aName, + Optional<uint64_t>(aVersion), + /* aDeleting */ false, aCallerType, aRv); +} + +RefPtr<IDBOpenDBRequest> IDBFactory::Open(JSContext* aCx, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + CallerType aCallerType, + ErrorResult& aRv) { + // This overload is nonstandard, see bug 1275496. + // Ignore calls with empty options for telemetry of usage count. + // Unfortunately, we cannot distinguish between the use of the method with + // only a single argument (which actually is a standard overload we don't want + // to count) an empty dictionary passed explicitly (which is the custom + // overload we would like to count). However, we assume that the latter is so + // rare that it can be neglected. + if (aOptions.IsAnyMemberPresent()) { + Telemetry::AccumulateCategorical(IdentifyPrincipalType(*mPrincipalInfo)); + } + + return OpenInternal(aCx, + /* aPrincipal */ nullptr, aName, aOptions.mVersion, + /* aDeleting */ false, aCallerType, aRv); +} + +RefPtr<IDBOpenDBRequest> IDBFactory::DeleteDatabase( + JSContext* aCx, const nsAString& aName, const IDBOpenDBOptions& aOptions, + CallerType aCallerType, ErrorResult& aRv) { + return OpenInternal(aCx, + /* aPrincipal */ nullptr, aName, Optional<uint64_t>(), + /* aDeleting */ true, aCallerType, aRv); +} + +int16_t IDBFactory::Cmp(JSContext* aCx, JS::Handle<JS::Value> aFirst, + JS::Handle<JS::Value> aSecond, ErrorResult& aRv) { + Key first, second; + auto result = first.SetFromJSVal(aCx, aFirst); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return 0; + } + + result = second.SetFromJSVal(aCx, aSecond); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return 0; + } + + if (first.IsUnset() || second.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return 0; + } + + return Key::CompareKeys(first, second); +} + +RefPtr<IDBOpenDBRequest> IDBFactory::OpenForPrincipal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + uint64_t aVersion, SystemCallerGuarantee aGuarantee, ErrorResult& aRv) { + MOZ_ASSERT(aPrincipal); + if (!NS_IsMainThread()) { + MOZ_CRASH( + "Figure out security checks for workers! What's this aPrincipal " + "we have on a worker thread?"); + } + + return OpenInternal(aCx, aPrincipal, aName, Optional<uint64_t>(aVersion), + /* aDeleting */ false, aGuarantee, aRv); +} + +RefPtr<IDBOpenDBRequest> IDBFactory::OpenForPrincipal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + const IDBOpenDBOptions& aOptions, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv) { + MOZ_ASSERT(aPrincipal); + if (!NS_IsMainThread()) { + MOZ_CRASH( + "Figure out security checks for workers! What's this aPrincipal " + "we have on a worker thread?"); + } + + return OpenInternal(aCx, aPrincipal, aName, aOptions.mVersion, + /* aDeleting */ false, aGuarantee, aRv); +} + +RefPtr<IDBOpenDBRequest> IDBFactory::DeleteForPrincipal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + const IDBOpenDBOptions& aOptions, SystemCallerGuarantee aGuarantee, + ErrorResult& aRv) { + MOZ_ASSERT(aPrincipal); + if (!NS_IsMainThread()) { + MOZ_CRASH( + "Figure out security checks for workers! What's this aPrincipal " + "we have on a worker thread?"); + } + + return OpenInternal(aCx, aPrincipal, aName, Optional<uint64_t>(), + /* aDeleting */ true, aGuarantee, aRv); +} + +RefPtr<IDBOpenDBRequest> IDBFactory::OpenInternal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + const Optional<uint64_t>& aVersion, bool aDeleting, CallerType aCallerType, + ErrorResult& aRv) { + if (NS_WARN_IF(!mGlobal)) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + CommonFactoryRequestParams commonParams; + + PrincipalInfo& principalInfo = commonParams.principalInfo(); + + if (aPrincipal) { + if (!NS_IsMainThread()) { + MOZ_CRASH( + "Figure out security checks for workers! What's this " + "aPrincipal we have on a worker thread?"); + } + MOZ_ASSERT(aCallerType == CallerType::System); + MOZ_DIAGNOSTIC_ASSERT(mPrivateBrowsingMode == + (aPrincipal->GetPrivateBrowsingId() > 0)); + + 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; + } + + if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(principalInfo))) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + } else { + if (mGlobal->GetStorageAccess() == StorageAccess::ePrivateBrowsing) { + if (NS_IsMainThread()) { + SetUseCounter( + mGlobal->GetGlobalJSObject(), + aDeleting + ? eUseCounter_custom_PrivateBrowsingIDBFactoryOpen + : eUseCounter_custom_PrivateBrowsingIDBFactoryDeleteDatabase); + } else { + SetUseCounter( + aDeleting ? UseCounterWorker::Custom_PrivateBrowsingIDBFactoryOpen + : UseCounterWorker:: + Custom_PrivateBrowsingIDBFactoryDeleteDatabase); + } + } + principalInfo = *mPrincipalInfo; + } + + uint64_t version = 0; + if (!aDeleting && aVersion.WasPassed()) { + if (aVersion.Value() < 1) { + aRv.ThrowTypeError("0 (Zero) is not a valid database 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; + + bool isInternal = principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo; + if (!isInternal && + principalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { + nsCString origin = + principalInfo.get_ContentPrincipalInfo().originNoSuffix(); + isInternal = QuotaManager::IsOriginInternal(origin); + } + + const bool isPrivate = + principalInfo.type() == PrincipalInfo::TContentPrincipalInfo && + principalInfo.get_ContentPrincipalInfo().attrs().mPrivateBrowsingId > 0; + + if (isInternal) { + // Chrome privilege and internal origins always get persistent storage. + persistenceType = PERSISTENCE_TYPE_PERSISTENT; + } else if (isPrivate) { + persistenceType = PERSISTENCE_TYPE_PRIVATE; + } else { + persistenceType = PERSISTENCE_TYPE_DEFAULT; + } + + 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) { + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + + UniquePtr<ThreadLocal> newIDBThreadLocal; + ThreadLocal* idbThreadLocal; + + if (threadLocal && threadLocal->mIndexedDBThreadLocal) { + idbThreadLocal = threadLocal->mIndexedDBThreadLocal.get(); + } else { + nsCOMPtr<nsIUUIDGenerator> uuidGen = + do_GetService("@mozilla.org/uuid-generator;1"); + MOZ_ASSERT(uuidGen); + + nsID id; + MOZ_ALWAYS_SUCCEEDS(uuidGen->GenerateUUIDInPlace(&id)); + + newIDBThreadLocal = WrapUnique(new ThreadLocal(id)); + idbThreadLocal = newIDBThreadLocal.get(); + } + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + { + BackgroundFactoryChild* actor = new BackgroundFactoryChild(*this); + + mBackgroundActor = static_cast<BackgroundFactoryChild*>( + backgroundActor->SendPBackgroundIDBFactoryConstructor( + actor, idbThreadLocal->GetLoggingInfo())); + + if (NS_WARN_IF(!mBackgroundActor)) { + mBackgroundActorFailed = true; + 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 = std::move(newIDBThreadLocal); + } + } + + RefPtr<IDBOpenDBRequest> request = IDBOpenDBRequest::Create( + aCx, SafeRefPtr{this, AcquireStrongRefFromRawPtr{}}, mGlobal); + if (!request) { + MOZ_ASSERT(!NS_IsMainThread()); + aRv.ThrowUncatchableException(); + return nullptr; + } + + MOZ_ASSERT(request); + + if (aDeleting) { + IDB_LOG_MARK_CHILD_REQUEST( + "indexedDB.deleteDatabase(\"%s\")", "IDBFactory.deleteDatabase(%.0s)", + request->LoggingSerialNumber(), NS_ConvertUTF16toUTF8(aName).get()); + } else { + IDB_LOG_MARK_CHILD_REQUEST( + "indexedDB.open(\"%s\", %s)", "IDBFactory.open(%.0s%.0s)", + request->LoggingSerialNumber(), NS_ConvertUTF16toUTF8(aName).get(), + IDB_LOG_STRINGIFY(aVersion)); + } + + nsresult rv = InitiateRequest(WrapNotNull(request), params); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; + } + + return request; +} + +nsresult IDBFactory::InitiateRequest( + const NotNull<RefPtr<IDBOpenDBRequest>>& aRequest, + const FactoryRequestParams& aParams) { + 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( + SafeRefPtr{this, AcquireStrongRefFromRawPtr{}}, 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; +} + +void IDBFactory::DisconnectFromGlobal(nsIGlobalObject* aOldGlobal) { + MOZ_DIAGNOSTIC_ASSERT(aOldGlobal); + // If CC unlinks us first, then mGlobal might be nullptr + MOZ_DIAGNOSTIC_ASSERT(!mGlobal || mGlobal == aOldGlobal); + + mGlobal = nullptr; +} + +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(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowserChild) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBFactory) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowserChild) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget) +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_END + +JSObject* IDBFactory::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return IDBFactory_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBFactory.h b/dom/indexedDB/IDBFactory.h new file mode 100644 index 0000000000..4fd25adf28 --- /dev/null +++ b/dom/indexedDB/IDBFactory.h @@ -0,0 +1,209 @@ +/* -*- 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/BindingDeclarations.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; +class nsIPrincipal; +class nsISerialEventTarget; +class nsPIDOMWindowInner; + +namespace mozilla { + +class ErrorResult; + +namespace ipc { + +class PBackgroundChild; +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +struct IDBOpenDBOptions; +class IDBOpenDBRequest; +template <typename> +class Optional; +class BrowserChild; +enum class CallerType : uint32_t; + +namespace indexedDB { +class BackgroundFactoryChild; +class FactoryRequestParams; +class LoggingInfo; +} // namespace indexedDB + +class IDBFactory final : public nsISupports, public nsWrapperCache { + using PBackgroundChild = mozilla::ipc::PBackgroundChild; + using PrincipalInfo = mozilla::ipc::PrincipalInfo; + + class BackgroundCreateCallback; + struct PendingRequestInfo; + struct IDBFactoryGuard {}; + + UniquePtr<PrincipalInfo> mPrincipalInfo; + + nsCOMPtr<nsIGlobalObject> mGlobal; + + // This will only be set if the factory belongs to a window in a child + // process. + RefPtr<BrowserChild> mBrowserChild; + + indexedDB::BackgroundFactoryChild* mBackgroundActor; + + // It is either set to a DocGroup-specific EventTarget if created by + // CreateForWindow() or set to GetCurrentSerialEventTarget() otherwise. + nsCOMPtr<nsISerialEventTarget> mEventTarget; + + uint64_t mInnerWindowID; + uint32_t mActiveTransactionCount; + uint32_t mActiveDatabaseCount; + + bool mBackgroundActorFailed; + bool mPrivateBrowsingMode; + + public: + explicit IDBFactory(const IDBFactoryGuard&); + + static Result<RefPtr<IDBFactory>, nsresult> CreateForWindow( + nsPIDOMWindowInner* aWindow); + + static Result<RefPtr<IDBFactory>, nsresult> CreateForMainThreadJS( + nsIGlobalObject* aGlobal); + + static Result<RefPtr<IDBFactory>, nsresult> CreateForWorker( + nsIGlobalObject* aGlobal, const PrincipalInfo& aPrincipalInfo, + uint64_t aInnerWindowID); + + static bool AllowedForWindow(nsPIDOMWindowInner* aWindow); + + static bool AllowedForPrincipal(nsIPrincipal* aPrincipal, + bool* aIsSystemPrincipal = nullptr); + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(IDBFactory); } + + nsISerialEventTarget* EventTarget() const { + AssertIsOnOwningThread(); + MOZ_RELEASE_ASSERT(mEventTarget); + return mEventTarget; + } + + void ClearBackgroundActor() { + AssertIsOnOwningThread(); + + mBackgroundActor = nullptr; + } + + // Increase/Decrease the number of active transactions for the decision + // making of preemption and throttling. + // Note: If the state of its actor is not committed or aborted, it could block + // IDB operations in other window. + void UpdateActiveTransactionCount(int32_t aDelta); + + // Increase/Decrease the number of active databases and IDBOpenRequests for + // the decision making of preemption and throttling. + // Note: A non-closed database or a pending IDBOpenRequest could block + // IDB operations in other window. + void UpdateActiveDatabaseCount(int32_t aDelta); + + nsIGlobalObject* GetParentObject() const { return mGlobal; } + + BrowserChild* GetBrowserChild() const { return mBrowserChild; } + + PrincipalInfo* GetPrincipalInfo() const { + AssertIsOnOwningThread(); + + return mPrincipalInfo.get(); + } + + uint64_t InnerWindowID() const { + AssertIsOnOwningThread(); + + return mInnerWindowID; + } + + bool IsChrome() const; + + [[nodiscard]] RefPtr<IDBOpenDBRequest> Open(JSContext* aCx, + const nsAString& aName, + uint64_t aVersion, + CallerType aCallerType, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBOpenDBRequest> Open(JSContext* aCx, + const nsAString& aName, + const IDBOpenDBOptions& aOptions, + CallerType aCallerType, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBOpenDBRequest> DeleteDatabase( + JSContext* aCx, const nsAString& aName, const IDBOpenDBOptions& aOptions, + CallerType aCallerType, ErrorResult& aRv); + + int16_t Cmp(JSContext* aCx, JS::Handle<JS::Value> aFirst, + JS::Handle<JS::Value> aSecond, ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBOpenDBRequest> OpenForPrincipal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + uint64_t aVersion, SystemCallerGuarantee, ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBOpenDBRequest> OpenForPrincipal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + const IDBOpenDBOptions& aOptions, SystemCallerGuarantee, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBOpenDBRequest> DeleteForPrincipal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + const IDBOpenDBOptions& aOptions, SystemCallerGuarantee, + ErrorResult& aRv); + + void DisconnectFromGlobal(nsIGlobalObject* aOldGlobal); + + 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(); + + static Result<RefPtr<IDBFactory>, nsresult> CreateForMainThreadJSInternal( + nsIGlobalObject* aGlobal, UniquePtr<PrincipalInfo> aPrincipalInfo); + + static Result<RefPtr<IDBFactory>, nsresult> CreateInternal( + nsIGlobalObject* aGlobal, UniquePtr<PrincipalInfo> aPrincipalInfo, + uint64_t aInnerWindowID); + + static nsresult AllowedForWindowInternal(nsPIDOMWindowInner* aWindow, + nsCOMPtr<nsIPrincipal>* aPrincipal); + + [[nodiscard]] RefPtr<IDBOpenDBRequest> OpenInternal( + JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, + const Optional<uint64_t>& aVersion, bool aDeleting, + CallerType aCallerType, ErrorResult& aRv); + + nsresult InitiateRequest(const NotNull<RefPtr<IDBOpenDBRequest>>& aRequest, + const indexedDB::FactoryRequestParams& aParams); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbfactory_h__ diff --git a/dom/indexedDB/IDBIndex.cpp b/dom/indexedDB/IDBIndex.cpp new file mode 100644 index 0000000000..2f77a1518c --- /dev/null +++ b/dom/indexedDB/IDBIndex.cpp @@ -0,0 +1,637 @@ +/* -*- 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 "IDBCursorType.h" +#include "IDBDatabase.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::dom { + +using namespace mozilla::dom::indexedDB; + +namespace { + +MovingNotNull<RefPtr<IDBRequest>> GenerateRequest(JSContext* aCx, + IDBIndex* aIndex) { + MOZ_ASSERT(aIndex); + aIndex->AssertIsOnOwningThread(); + + auto transaction = aIndex->ObjectStore()->AcquireTransaction(); + auto* const database = transaction->Database(); + + return IDBRequest::Create(aCx, aIndex, database, std::move(transaction)); +} + +} // 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) { + mozilla::DropJSObjects(this); + } +} + +RefPtr<IDBIndex> IDBIndex::Create(IDBObjectStore* aObjectStore, + const IndexMetadata& aMetadata) { + MOZ_ASSERT(aObjectStore); + aObjectStore->AssertIsOnOwningThread(); + + return new IDBIndex(aObjectStore, &aMetadata); +} + +#ifdef DEBUG + +void IDBIndex::AssertIsOnOwningThread() const { + MOZ_ASSERT(mObjectStore); + mObjectStore->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +RefPtr<IDBRequest> IDBIndex::OpenCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ false, aCx, aRange, aDirection, + aRv); +} + +RefPtr<IDBRequest> IDBIndex::OpenKeyCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ true, aCx, aRange, aDirection, aRv); +} + +RefPtr<IDBRequest> IDBIndex::Get(JSContext* aCx, JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ false, aCx, aKey, aRv); +} + +RefPtr<IDBRequest> IDBIndex::GetKey(JSContext* aCx, JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ true, aCx, aKey, aRv); +} + +RefPtr<IDBRequest> IDBIndex::GetAll(JSContext* aCx, JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ false, aCx, aKey, aLimit, aRv); +} + +RefPtr<IDBRequest> IDBIndex::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 IDBIndex::RefreshMetadata(bool aMayDelete) { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mDeletedMetadata, mMetadata == mDeletedMetadata.get()); + + const auto& indexes = mObjectStore->Spec().indexes(); + const auto foundIt = std::find_if( + indexes.cbegin(), indexes.cend(), + [id = Id()](const auto& metadata) { return metadata.id() == id; }); + const bool found = foundIt != indexes.cend(); + + MOZ_ASSERT_IF(!aMayDelete && !mDeletedMetadata, found); + + if (found) { + mMetadata = &*foundIt; + MOZ_ASSERT(mMetadata != mDeletedMetadata.get()); + mDeletedMetadata = nullptr; + } else { + NoteDeletion(); + } +} + +void IDBIndex::NoteDeletion() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + MOZ_ASSERT(Id() == mMetadata->id()); + + if (mDeletedMetadata) { + MOZ_ASSERT(mMetadata == mDeletedMetadata.get()); + return; + } + + mDeletedMetadata = MakeUnique<IndexMetadata>(*mMetadata); + + mMetadata = mDeletedMetadata.get(); +} + +const nsString& IDBIndex::Name() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->name(); +} + +void IDBIndex::SetName(const nsAString& aName, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + const auto& transaction = mObjectStore->TransactionRef(); + + if (transaction.GetMode() != IDBTransaction::Mode::VersionChange || + mDeletedMetadata) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (!transaction.IsActive()) { + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "rename(%s)", + "IDBIndex.rename(%.0s%.0s%.0s%.0s%.0s)", + transaction.LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(transaction.Database()), IDB_LOG_STRINGIFY(transaction), + IDB_LOG_STRINGIFY(mObjectStore), loggingOldIndex.get(), + IDB_LOG_STRINGIFY(this)); + + mObjectStore->MutableTransactionRef().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 { + CopyASCIItoUTF16(mMetadata->locale(), aLocale); + } +} + +const nsCString& IDBIndex::Locale() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->locale(); +} + +bool IDBIndex::IsAutoLocale() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mMetadata); + + return mMetadata->autoLocale(); +} + +nsIGlobalObject* 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); +} + +RefPtr<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; + } + + const auto& transaction = mObjectStore->TransactionRef(); + if (!transaction.IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + 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_KEY_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); + } + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (aKeyOnly) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "getKey(%s)", + "IDBIndex.getKey(%.0s%.0s%.0s%.0s%.0s)", + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "get(%s)", + "IDBIndex.get(%.0s%.0s%.0s%.0s%.0s)", 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)); + } + + auto& mutableTransaction = mObjectStore->MutableTransactionRef(); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mutableTransaction.InvalidateCursorCaches(); + + mutableTransaction.StartRequest(request, params); + + return request; +} + +RefPtr<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; + } + + const auto& transaction = mObjectStore->TransactionRef(); + if (!transaction.IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + const int64_t objectStoreId = mObjectStore->Id(); + const int64_t indexId = Id(); + + Maybe<SerializedKeyRange> optionalKeyRange; + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + optionalKeyRange.emplace(serializedKeyRange); + } + + const uint32_t limit = aLimit.WasPassed() ? aLimit.Value() : 0; + + const auto& params = + aKeysOnly ? RequestParams{IndexGetAllKeysParams(objectStoreId, indexId, + optionalKeyRange, limit)} + : RequestParams{IndexGetAllParams(objectStoreId, indexId, + optionalKeyRange, limit)}; + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (aKeysOnly) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "getAllKeys(%s, %s)", + "IDBIndex.getAllKeys(%.0s%.0s%.0s%.0s%.0s%.0s)", + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "getAll(%s, %s)", + "IDBIndex.getAll(%.0s%.0s%.0s%.0s%.0s%.0s)", + 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)); + } + + auto& mutableTransaction = mObjectStore->MutableTransactionRef(); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mutableTransaction.InvalidateCursorCaches(); + + mutableTransaction.StartRequest(request, params); + + return request; +} + +RefPtr<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; + } + + const auto& transaction = mObjectStore->TransactionRef(); + if (!transaction.IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aRange, &keyRange, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + const int64_t objectStoreId = mObjectStore->Id(); + const int64_t indexId = Id(); + + Maybe<SerializedKeyRange> optionalKeyRange; + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + optionalKeyRange.emplace(std::move(serializedKeyRange)); + } + + const CommonIndexOpenCursorParams commonIndexParams = { + {objectStoreId, std::move(optionalKeyRange), aDirection}, indexId}; + + const auto params = + aKeysOnly ? OpenCursorParams{IndexOpenKeyCursorParams{commonIndexParams}} + : OpenCursorParams{IndexOpenCursorParams{commonIndexParams}}; + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (aKeysOnly) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "openKeyCursor(%s, %s)", + "IDBIndex.openKeyCursor(%.0s%.0s%.0s%.0s%.0s%.0s)", + 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(aDirection)); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "openCursor(%s, %s)", + "IDBIndex.openCursor(%.0s%.0s%.0s%.0s%.0s%.0s)", + 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(aDirection)); + } + + const auto actor = + aKeysOnly + ? static_cast<SafeRefPtr<BackgroundCursorChildBase>>( + MakeSafeRefPtr<BackgroundCursorChild<IDBCursorType::IndexKey>>( + request, this, aDirection)) + : MakeSafeRefPtr<BackgroundCursorChild<IDBCursorType::Index>>( + request, this, aDirection); + + auto& mutableTransaction = mObjectStore->MutableTransactionRef(); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mutableTransaction.InvalidateCursorCaches(); + + mutableTransaction.OpenCursor(*actor, params); + + return request; +} + +RefPtr<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; + } + + const auto& transaction = mObjectStore->TransactionRef(); + if (!transaction.IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + if (aRv.Failed()) { + return nullptr; + } + + IndexCountParams params; + params.objectStoreId() = mObjectStore->Id(); + params.indexId() = Id(); + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + params.optionalKeyRange().emplace(serializedKeyRange); + } + + auto request = GenerateRequest(aCx, this).unwrap(); + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).index(%s)." + "count(%s)", + "IDBIndex.count(%.0s%.0s%.0s%.0s%.0s)", 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)); + + auto& mutableTransaction = mObjectStore->MutableTransactionRef(); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mutableTransaction.InvalidateCursorCaches(); + + mutableTransaction.StartRequest(request, params); + + return request; +} + +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(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 IDBIndex_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBIndex.h b/dom/indexedDB/IDBIndex.h new file mode 100644 index 0000000000..4071e81cd2 --- /dev/null +++ b/dom/indexedDB/IDBIndex.h @@ -0,0 +1,173 @@ +/* -*- 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 "mozilla/UniquePtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsTArrayForwardDeclare.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +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 { + // TODO: This could be made const if Bug 1575173 is resolved. It is + // initialized in the constructor and never modified/cleared. + 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; + UniquePtr<indexedDB::IndexMetadata> mDeletedMetadata; + + const int64_t mId; + bool mRooted; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(IDBIndex) + + [[nodiscard]] static RefPtr<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; + } + + nsIGlobalObject* 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); + + [[nodiscard]] RefPtr<IDBRequest> OpenCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> OpenKeyCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Get(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetKey(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Count(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetAll(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetAllKeys(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& 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(); + + [[nodiscard]] RefPtr<IDBRequest> GetInternal(bool aKeyOnly, JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetAllInternal( + bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, ErrorResult& aRv); + + [[nodiscard]] RefPtr<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 0000000000..9719dd4685 --- /dev/null +++ b/dom/indexedDB/IDBKeyRange.cpp @@ -0,0 +1,348 @@ +/* -*- 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/HoldDropJSObjects.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/IDBKeyRangeBinding.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" + +namespace mozilla::dom { + +using namespace mozilla::dom::indexedDB; + +namespace { + +void GetKeyFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal, Key& aKey, + ErrorResult& aRv) { + auto result = aKey.SetFromJSVal(aCx, aVal); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return; + } + + if (aKey.IsUnset()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + } +} + +} // 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) { + AssertIsOnOwningThread(); +} + +IDBKeyRange::~IDBKeyRange() { DropJSObjects(); } + +IDBLocaleAwareKeyRange::IDBLocaleAwareKeyRange(nsISupports* aGlobal, + bool aLowerOpen, bool aUpperOpen, + bool aIsOnly) + : IDBKeyRange(aGlobal, aLowerOpen, aUpperOpen, aIsOnly) { + AssertIsOnOwningThread(); +} + +IDBLocaleAwareKeyRange::~IDBLocaleAwareKeyRange() { DropJSObjects(); } + +// static +void IDBKeyRange::FromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal, + RefPtr<IDBKeyRange>* aKeyRange, ErrorResult& aRv) { + MOZ_ASSERT_IF(!aCx, aVal.isUndefined()); + MOZ_ASSERT(aKeyRange); + + RefPtr<IDBKeyRange> keyRange; + + if (aVal.isNullOrUndefined()) { + // undefined and null returns no IDBKeyRange. + *aKeyRange = std::move(keyRange); + return; + } + + JS::Rooted<JSObject*> obj(aCx, aVal.isObject() ? &aVal.toObject() : nullptr); + + // Unwrap an IDBKeyRange object if possible. + if (obj && NS_SUCCEEDED(UNWRAP_OBJECT(IDBKeyRange, obj, keyRange))) { + MOZ_ASSERT(keyRange); + *aKeyRange = std::move(keyRange); + return; + } + + // A valid key returns an 'only' IDBKeyRange. + keyRange = new IDBKeyRange(nullptr, false, false, true); + GetKeyFromJSVal(aCx, aVal, keyRange->Lower(), aRv); + if (!aRv.Failed()) { + *aKeyRange = std::move(keyRange); + } +} + +// static +RefPtr<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; +} + +void IDBKeyRange::ToSerialized(SerializedKeyRange& aKeyRange) const { + aKeyRange.lowerOpen() = LowerOpen(); + aKeyRange.upperOpen() = UpperOpen(); + aKeyRange.isOnly() = IsOnly(); + + aKeyRange.lower() = Lower(); + if (!IsOnly()) { + aKeyRange.upper() = Upper(); + } +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBKeyRange) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBKeyRange) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +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) + +void IDBKeyRange::DropJSObjects() { + if (!mRooted) { + return; + } + mHaveCachedLowerVal = false; + mHaveCachedUpperVal = false; + mRooted = false; + mozilla::DropJSObjects(this); +} + +bool IDBKeyRange::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) { + return IDBKeyRange_Binding::Wrap(aCx, this, aGivenProto, aReflector); +} + +bool IDBLocaleAwareKeyRange::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) { + return IDBLocaleAwareKeyRange_Binding::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; + GetKeyFromJSVal(aCx, aValue, key, aRv); + 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 +RefPtr<IDBKeyRange> IDBKeyRange::Only(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + RefPtr<IDBKeyRange> keyRange = + new IDBKeyRange(aGlobal.GetAsSupports(), false, false, true); + + GetKeyFromJSVal(aGlobal.Context(), aValue, keyRange->Lower(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + return keyRange; +} + +// static +RefPtr<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); + + GetKeyFromJSVal(aGlobal.Context(), aValue, keyRange->Lower(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + return keyRange; +} + +// static +RefPtr<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); + + GetKeyFromJSVal(aGlobal.Context(), aValue, keyRange->Upper(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + return keyRange; +} + +// static +RefPtr<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); + + GetKeyFromJSVal(aGlobal.Context(), aLower, keyRange->Lower(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + GetKeyFromJSVal(aGlobal.Context(), aUpper, keyRange->Upper(), aRv); + 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; +} + +// static +RefPtr<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); + + GetKeyFromJSVal(aGlobal.Context(), aLower, keyRange->Lower(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + GetKeyFromJSVal(aGlobal.Context(), aUpper, keyRange->Upper(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (keyRange->Lower() == keyRange->Upper() && (aLowerOpen || aUpperOpen)) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return nullptr; + } + + return keyRange; +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBKeyRange.h b/dom/indexedDB/IDBKeyRange.h new file mode 100644 index 0000000000..647c45eb98 --- /dev/null +++ b/dom/indexedDB/IDBKeyRange.h @@ -0,0 +1,143 @@ +/* -*- 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; + +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; + + 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 void FromJSVal(JSContext* aCx, JS::Handle<JS::Value> aVal, + RefPtr<IDBKeyRange>* aKeyRange, ErrorResult& aRv); + + [[nodiscard]] static RefPtr<IDBKeyRange> FromSerialized( + const indexedDB::SerializedKeyRange& aKeyRange); + + [[nodiscard]] static RefPtr<IDBKeyRange> Only(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv); + + [[nodiscard]] static RefPtr<IDBKeyRange> LowerBound( + const GlobalObject& aGlobal, JS::Handle<JS::Value> aValue, bool aOpen, + ErrorResult& aRv); + + [[nodiscard]] static RefPtr<IDBKeyRange> UpperBound( + const GlobalObject& aGlobal, JS::Handle<JS::Value> aValue, bool aOpen, + ErrorResult& aRv); + + [[nodiscard]] static RefPtr<IDBKeyRange> Bound(const GlobalObject& aGlobal, + JS::Handle<JS::Value> aLower, + JS::Handle<JS::Value> aUpper, + bool aLowerOpen, + bool aUpperOpen, + ErrorResult& aRv); + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(IDBKeyRange); } + + 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> aValue, + ErrorResult& aRv) const; + + bool IsOnly() const { return mIsOnly; } + + 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: + [[nodiscard]] static RefPtr<IDBLocaleAwareKeyRange> Bound( + const GlobalObject& aGlobal, JS::Handle<JS::Value> aLower, + JS::Handle<JS::Value> aUpper, bool aLowerOpen, bool aUpperOpen, + ErrorResult& aRv); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(IDBLocaleAwareKeyRange, IDBKeyRange) + + // 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/IDBObjectStore.cpp b/dom/indexedDB/IDBObjectStore.cpp new file mode 100644 index 0000000000..9a7ef25fc4 --- /dev/null +++ b/dom/indexedDB/IDBObjectStore.cpp @@ -0,0 +1,1740 @@ +/* -*- 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 <numeric> +#include <utility> + +#include "IDBCursorType.h" +#include "IDBDatabase.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBIndex.h" +#include "IDBKeyRange.h" +#include "IDBRequest.h" +#include "IDBTransaction.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDatabaseManager.h" +#include "IndexedDBCommon.h" +#include "KeyPath.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject +#include "js/Class.h" +#include "js/Date.h" +#include "js/Object.h" // JS::GetClass +#include "js/PropertyAndElement.h" // JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_HasOwnPropertyById +#include "js/StructuredClone.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IDBObjectStoreBinding.h" +#include "mozilla/dom/MemoryBlobImpl.h" +#include "mozilla/dom/StreamBlobImpl.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/StructuredCloneTags.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsCOMPtr.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace mozilla::dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; + +namespace { + +Result<IndexUpdateInfo, nsresult> MakeIndexUpdateInfo( + const int64_t aIndexID, const Key& aKey, const nsCString& aLocale) { + IndexUpdateInfo indexUpdateInfo; + indexUpdateInfo.indexId() = aIndexID; + indexUpdateInfo.value() = aKey; + if (!aLocale.IsEmpty()) { + QM_TRY_UNWRAP(indexUpdateInfo.localizedValue(), + aKey.ToLocaleAwareKey(aLocale)); + } + return indexUpdateInfo; +} + +} // namespace + +struct IDBObjectStore::StructuredCloneWriteInfo { + JSAutoStructuredCloneBuffer mCloneBuffer; + nsTArray<StructuredCloneFileChild> mFiles; + IDBDatabase* mDatabase; + uint64_t mOffsetToKeyProp; + + explicit StructuredCloneWriteInfo(IDBDatabase* aDatabase) + : mCloneBuffer(JS::StructuredCloneScope::DifferentProcessForIndexedDB, + nullptr, nullptr), + mDatabase(aDatabase), + mOffsetToKeyProp(0) { + MOZ_ASSERT(aDatabase); + + MOZ_COUNT_CTOR(StructuredCloneWriteInfo); + } + + StructuredCloneWriteInfo(StructuredCloneWriteInfo&& aCloneWriteInfo) noexcept + : mCloneBuffer(std::move(aCloneWriteInfo.mCloneBuffer)), + mFiles(std::move(aCloneWriteInfo.mFiles)), + mDatabase(aCloneWriteInfo.mDatabase), + mOffsetToKeyProp(aCloneWriteInfo.mOffsetToKeyProp) { + MOZ_ASSERT(mDatabase); + + MOZ_COUNT_CTOR(StructuredCloneWriteInfo); + + aCloneWriteInfo.mOffsetToKeyProp = 0; + } + + MOZ_COUNTED_DTOR(StructuredCloneWriteInfo) +}; + +// Used by ValueWrapper::Clone to hold strong references to any blob-like +// objects through the clone process. This is necessary because: +// - The structured clone process may trigger content code via getters/other +// which can potentially cause existing strong references to be dropped, +// necessitating the clone to hold its own strong references. +// - The structured clone can abort partway through, so it's necessary to track +// what strong references have been acquired so that they can be freed even +// if a de-serialization does not occur. +struct IDBObjectStore::StructuredCloneInfo { + nsTArray<StructuredCloneFileChild> mFiles; +}; + +namespace { + +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); + } + + MOZ_COUNTED_DTOR(GetAddInfoClosure) +}; + +MovingNotNull<RefPtr<IDBRequest>> GenerateRequest( + JSContext* aCx, IDBObjectStore* aObjectStore) { + MOZ_ASSERT(aObjectStore); + aObjectStore->AssertIsOnOwningThread(); + + auto transaction = aObjectStore->AcquireTransaction(); + auto* const database = transaction->Database(); + + return IDBRequest::Create(aCx, aObjectStore, database, + std::move(transaction)); +} + +bool StructuredCloneWriteCallback(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle<JSObject*> aObj, + bool* aSameProcessRequired, void* aClosure) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWriter); + MOZ_ASSERT(aClosure); + + auto* const 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); + + { + Blob* blob = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { + ErrorResult rv; + const uint64_t nativeEndianSize = blob->GetSize(rv); + MOZ_ASSERT(!rv.Failed()); + + const uint64_t size = NativeEndian::swapToLittleEndian(nativeEndianSize); + + nsString type; + blob->GetType(type); + + const NS_ConvertUTF16toUTF8 convType(type); + const 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; + } + + const RefPtr<File> file = blob->ToFile(); + if (file) { + ErrorResult rv; + const int64_t nativeEndianLastModifiedDate = file->GetLastModified(rv); + MOZ_ALWAYS_TRUE(!rv.Failed()); + + const int64_t lastModifiedDate = + NativeEndian::swapToLittleEndian(nativeEndianLastModifiedDate); + + nsString name; + file->GetName(name); + + const NS_ConvertUTF16toUTF8 convName(name); + const 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; + } + } + + cloneWriteInfo->mFiles.EmplaceBack(StructuredCloneFileBase::eBlob, blob); + + return true; + } + } + + return StructuredCloneHolder::WriteFullySerializableObjects(aCx, aWriter, + aObj); +} + +bool CopyingStructuredCloneWriteCallback(JSContext* aCx, + JSStructuredCloneWriter* aWriter, + JS::Handle<JSObject*> aObj, + bool* aSameProcessRequired, + void* aClosure) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWriter); + MOZ_ASSERT(aClosure); + + auto* const cloneInfo = + static_cast<IDBObjectStore::StructuredCloneInfo*>(aClosure); + + // UNWRAP_OBJECT calls might mutate this. + JS::Rooted<JSObject*> obj(aCx, aObj); + + { + Blob* blob = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { + if (cloneInfo->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 = cloneInfo->mFiles.Length(); + + if (!JS_WriteUint32Pair(aWriter, + blob->IsFile() ? SCTAG_DOM_FILE : SCTAG_DOM_BLOB, + index)) { + return false; + } + + cloneInfo->mFiles.EmplaceBack(StructuredCloneFileBase::eBlob, blob); + + 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 */, + nullptr /* canTransfer */, nullptr /* sabCloned */ + }; + + MOZ_ASSERT(aCx); + + auto* const 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; +} + +using indexedDB::WrapAsJSObject; + +template <typename T> +JSObject* WrapAsJSObject(JSContext* const aCx, T& aBaseObject) { + JS::Rooted<JSObject*> result(aCx); + const bool res = WrapAsJSObject(aCx, aBaseObject, &result); + return res ? static_cast<JSObject*>(result) : nullptr; +} + +JSObject* CopyingStructuredCloneReadCallback( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, + void* aClosure) { + MOZ_ASSERT(aTag != SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE); + + if (aTag == SCTAG_DOM_BLOB || aTag == SCTAG_DOM_FILE || + aTag == SCTAG_DOM_MUTABLEFILE) { + auto* const cloneInfo = + static_cast<IDBObjectStore::StructuredCloneInfo*>(aClosure); + + if (aData >= cloneInfo->mFiles.Length()) { + MOZ_ASSERT(false, "Bad index value!"); + return nullptr; + } + + StructuredCloneFileChild& file = cloneInfo->mFiles[aData]; + + switch (static_cast<StructuredCloneTags>(aTag)) { + case SCTAG_DOM_BLOB: + MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eBlob); + MOZ_ASSERT(!file.Blob().IsFile()); + + return WrapAsJSObject(aCx, file.MutableBlob()); + + case SCTAG_DOM_FILE: { + MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eBlob); + + JS::Rooted<JSObject*> result(aCx); + + { + // Create a scope so ~RefPtr fires before returning an unwrapped + // JS::Value. + const RefPtr<Blob> blob = file.BlobPtr(); + MOZ_ASSERT(blob->IsFile()); + + const RefPtr<File> file = blob->ToFile(); + MOZ_ASSERT(file); + + if (!WrapAsJSObject(aCx, file, &result)) { + return nullptr; + } + } + + return result; + } + + case SCTAG_DOM_MUTABLEFILE: + MOZ_ASSERT(file.Type() == StructuredCloneFileBase::eMutableFile); + + return nullptr; + + default: + // This cannot be reached due to the if condition before. + break; + } + } + + return StructuredCloneHolder::ReadFullySerializableObjects(aCx, aReader, + aTag); +} + +} // namespace + +const JSClass IDBObjectStore::sDummyPropJSClass = { + "IDBObjectStore Dummy", 0 /* flags */ +}; + +IDBObjectStore::IDBObjectStore(SafeRefPtr<IDBTransaction> aTransaction, + ObjectStoreSpec* aSpec) + : mTransaction(std::move(aTransaction)), + mCachedKeyPath(JS::UndefinedValue()), + mSpec(aSpec), + mId(aSpec->metadata().id()), + mRooted(false) { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); + MOZ_ASSERT(aSpec); +} + +IDBObjectStore::~IDBObjectStore() { + AssertIsOnOwningThread(); + + if (mRooted) { + mozilla::DropJSObjects(this); + } +} + +// static +RefPtr<IDBObjectStore> IDBObjectStore::Create( + SafeRefPtr<IDBTransaction> aTransaction, ObjectStoreSpec& aSpec) { + MOZ_ASSERT(aTransaction); + aTransaction->AssertIsOnOwningThread(); + + return new IDBObjectStore(std::move(aTransaction), &aSpec); +} + +// static +void IDBObjectStore::AppendIndexUpdateInfo( + const int64_t aIndexID, const KeyPath& aKeyPath, const bool aMultiEntry, + const nsCString& aLocale, JSContext* const aCx, JS::Handle<JS::Value> aVal, + nsTArray<IndexUpdateInfo>* const aUpdateInfoArray, ErrorResult* const aRv) { + // This precondition holds when `aVal` is the result of a structured clone. + js::AutoAssertNoContentJS noContentJS(aCx); + + if (!aMultiEntry) { + Key key; + *aRv = aKeyPath.ExtractKey(aCx, aVal, key); + + // If an index's keyPath doesn't match an object, we ignore that object. + if (aRv->ErrorCodeIs(NS_ERROR_DOM_INDEXEDDB_DATA_ERR) || key.IsUnset()) { + aRv->SuppressException(); + return; + } + + if (aRv->Failed()) { + return; + } + + QM_TRY_UNWRAP(auto item, MakeIndexUpdateInfo(aIndexID, key, aLocale), + QM_VOID, + [aRv](const nsresult tryResult) { aRv->Throw(tryResult); }); + + aUpdateInfoArray->AppendElement(std::move(item)); + return; + } + + JS::Rooted<JS::Value> val(aCx); + if (NS_FAILED(aKeyPath.ExtractKeyAsJSVal(aCx, aVal, val.address()))) { + return; + } + + bool isArray; + if (NS_WARN_IF(!JS::IsArrayObject(aCx, val, &isArray))) { + IDB_REPORT_INTERNAL_ERR(); + aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + 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(); + aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + for (uint32_t arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) { + JS::Rooted<JS::PropertyKey> indexId(aCx); + if (NS_WARN_IF(!JS_IndexToId(aCx, arrayIndex, &indexId))) { + IDB_REPORT_INTERNAL_ERR(); + aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + bool hasOwnProperty; + if (NS_WARN_IF( + !JS_HasOwnPropertyById(aCx, array, indexId, &hasOwnProperty))) { + IDB_REPORT_INTERNAL_ERR(); + aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + if (!hasOwnProperty) { + continue; + } + + JS::Rooted<JS::Value> arrayItem(aCx); + if (NS_WARN_IF(!JS_GetPropertyById(aCx, array, indexId, &arrayItem))) { + IDB_REPORT_INTERNAL_ERR(); + aRv->Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + Key value; + auto result = value.SetFromJSVal(aCx, arrayItem); + if (result.isErr() || value.IsUnset()) { + // Not a value we can do anything with, ignore it. + if (result.isErr() && + result.inspectErr().Is(SpecialValues::Exception)) { + result.unwrapErr().AsException().SuppressException(); + } + continue; + } + + QM_TRY_UNWRAP(auto item, MakeIndexUpdateInfo(aIndexID, value, aLocale), + QM_VOID, + [aRv](const nsresult tryResult) { aRv->Throw(tryResult); }); + + aUpdateInfoArray->AppendElement(std::move(item)); + } + } else { + Key value; + auto result = value.SetFromJSVal(aCx, val); + if (result.isErr() || value.IsUnset()) { + // Not a value we can do anything with, ignore it. + if (result.isErr() && result.inspectErr().Is(SpecialValues::Exception)) { + result.unwrapErr().AsException().SuppressException(); + } + return; + } + + QM_TRY_UNWRAP(auto item, MakeIndexUpdateInfo(aIndexID, value, aLocale), + QM_VOID, + [aRv](const nsresult tryResult) { aRv->Throw(tryResult); }); + + aUpdateInfoArray->AppendElement(std::move(item)); + } +} + +// static +void IDBObjectStore::ClearCloneReadInfo( + StructuredCloneReadInfoChild& 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.HasFiles()) { + return; + } + + aReadInfo.ReleaseFiles(); +} + +// static +bool IDBObjectStore::DeserializeValue( + JSContext* aCx, StructuredCloneReadInfoChild&& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue) { + MOZ_ASSERT(aCx); + + if (!aCloneReadInfo.Data().Size()) { + aValue.setUndefined(); + return true; + } + + MOZ_ASSERT(!(aCloneReadInfo.Data().Size() % sizeof(uint64_t))); + + static const JSStructuredCloneCallbacks callbacks = { + StructuredCloneReadCallback<StructuredCloneReadInfoChild>, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr}; + + // FIXME: Consider to use StructuredCloneHolder here and in other + // deserializing methods. + return JS_ReadStructuredClone( + aCx, aCloneReadInfo.Data(), JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::DifferentProcessForIndexedDB, aValue, + JS::CloneDataPolicy(), &callbacks, &aCloneReadInfo); +} + +#ifdef DEBUG + +void IDBObjectStore::AssertIsOnOwningThread() const { + MOZ_ASSERT(mTransaction); + mTransaction->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void IDBObjectStore::GetAddInfo(JSContext* aCx, ValueWrapper& aValueWrapper, + JS::Handle<JS::Value> aKeyVal, + StructuredCloneWriteInfo& aCloneWriteInfo, + Key& aKey, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray, + ErrorResult& aRv) { + // Return DATA_ERR if a key was passed in and this objectStore uses inline + // keys. + if (!aKeyVal.isUndefined() && HasValidKeyPath()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + + const bool isAutoIncrement = AutoIncrement(); + + if (!HasValidKeyPath()) { + // Out-of-line keys must be passed in. + auto result = aKey.SetFromJSVal(aCx, aKeyVal); + if (result.isErr()) { + aRv = result.unwrapErr().ExtractErrorResult( + InvalidMapsTo<NS_ERROR_DOM_INDEXEDDB_DATA_ERR>); + return; + } + } else if (!isAutoIncrement) { + if (!aValueWrapper.Clone(aCx)) { + aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); + return; + } + + aRv = GetKeyPath().ExtractKey(aCx, aValueWrapper.Value(), aKey); + if (aRv.Failed()) { + return; + } + } + + // Return DATA_ERR if no key was specified this isn't an autoIncrement + // objectStore. + if (aKey.IsUnset() && !isAutoIncrement) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); + return; + } + + // Figure out indexes and the index values to update here. + + if (mSpec->indexes().Length() && !aValueWrapper.Clone(aCx)) { + aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); + return; + } + + { + 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]; + + AppendIndexUpdateInfo(metadata.id(), metadata.keyPath(), + metadata.multiEntry(), metadata.locale(), aCx, + aValueWrapper.Value(), &aUpdateInfoArray, &aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + } + + if (isAutoIncrement && HasValidKeyPath()) { + if (!aValueWrapper.Clone(aCx)) { + aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR); + return; + } + + GetAddInfoClosure data(aCloneWriteInfo, aValueWrapper.Value()); + + MOZ_ASSERT(aKey.IsUnset()); + + aRv = GetKeyPath().ExtractOrCreateKey(aCx, aValueWrapper.Value(), aKey, + &GetAddInfoCallback, &data); + } else { + GetAddInfoClosure data(aCloneWriteInfo, aValueWrapper.Value()); + + aRv = GetAddInfoCallback(aCx, &data); + } +} + +RefPtr<IDBRequest> IDBObjectStore::AddOrPut(JSContext* aCx, + ValueWrapper& aValueWrapper, + JS::Handle<JS::Value> aKey, + bool aOverwrite, bool aFromCursor, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aCx); + MOZ_ASSERT_IF(aFromCursor, aOverwrite); + + if (mTransaction->GetMode() == IDBTransaction::Mode::Cleanup || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsActive()) { + 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; + } + + Key key; + StructuredCloneWriteInfo cloneWriteInfo(mTransaction->Database()); + nsTArray<IndexUpdateInfo> updateInfos; + + // According to spec https://w3c.github.io/IndexedDB/#clone-value, + // the transaction must be in inactive state during clone + mTransaction->TransitionToInactive(); + +#ifdef DEBUG + const uint32_t previousPendingRequestCount{ + mTransaction->GetPendingRequestCount()}; +#endif + GetAddInfo(aCx, aValueWrapper, aKey, cloneWriteInfo, key, updateInfos, aRv); + // Check that new requests were rejected in the Inactive state + // and possibly in the Finished state, if the transaction has been aborted, + // during the structured cloning. + MOZ_ASSERT(mTransaction->GetPendingRequestCount() == + previousPendingRequestCount); + + if (!mTransaction->IsAborted()) { + mTransaction->TransitionToActive(); + } else if (!aRv.Failed()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR); + return nullptr; // It is mandatory to return right after throw + } + + 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; + + const size_t indexUpdateInfoSize = + std::accumulate(updateInfos.cbegin(), updateInfos.cend(), 0u, + [](size_t old, const IndexUpdateInfo& updateInfo) { + return old + updateInfo.value().GetBuffer().Length() + + updateInfo.localizedValue().GetBuffer().Length(); + }); + + const size_t messageSize = cloneWriteInfo.mCloneBuffer.data().Size() + + key.GetBuffer().Length() + indexUpdateInfoSize; + + if (messageSize > kMaxMessageSize) { + IDB_REPORT_INTERNAL_ERR(); + aRv.ThrowUnknownError( + 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 = + std::move(cloneWriteInfo.mCloneBuffer.data()); + commonParams.cloneInfo().offsetToKeyProp() = cloneWriteInfo.mOffsetToKeyProp; + commonParams.key() = key; + commonParams.indexUpdateInfos() = std::move(updateInfos); + + // Convert any blobs or mutable files into FileAddInfo. + QM_TRY_UNWRAP( + commonParams.fileAddInfos(), + TransformIntoNewArrayAbortOnErr( + cloneWriteInfo.mFiles, + [&database = *mTransaction->Database()]( + auto& file) -> Result<FileAddInfo, nsresult> { + switch (file.Type()) { + case StructuredCloneFileBase::eBlob: { + MOZ_ASSERT(file.HasBlob()); + + PBackgroundIDBDatabaseFileChild* const fileActor = + database.GetOrCreateFileActorForBlob(file.MutableBlob()); + if (NS_WARN_IF(!fileActor)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + return FileAddInfo{WrapNotNull(fileActor), + StructuredCloneFileBase::eBlob}; + } + + case StructuredCloneFileBase::eWasmBytecode: + case StructuredCloneFileBase::eWasmCompiled: { + MOZ_ASSERT(file.HasBlob()); + + PBackgroundIDBDatabaseFileChild* const fileActor = + database.GetOrCreateFileActorForBlob(file.MutableBlob()); + if (NS_WARN_IF(!fileActor)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + + return FileAddInfo{WrapNotNull(fileActor), file.Type()}; + } + + default: + MOZ_CRASH("Should never get here!"); + } + }, + fallible), + nullptr, [&aRv](const nsresult result) { aRv = result; }); + + const auto& params = + aOverwrite ? RequestParams{ObjectStorePutParams(std::move(commonParams))} + : RequestParams{ObjectStoreAddParams(std::move(commonParams))}; + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (!aFromCursor) { + if (aOverwrite) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).put(%s)", + "IDBObjectStore.put(%.0s%.0s%.0s%.0s)", + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).add(%s)", + "IDBObjectStore.add(%.0s%.0s%.0s%.0s)", + 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); + + mTransaction->InvalidateCursorCaches(); + + return request; +} + +RefPtr<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->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + const int64_t id = Id(); + + Maybe<SerializedKeyRange> optionalKeyRange; + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + optionalKeyRange.emplace(serializedKeyRange); + } + + const uint32_t limit = aLimit.WasPassed() ? aLimit.Value() : 0; + + RequestParams params; + if (aKeysOnly) { + params = ObjectStoreGetAllKeysParams(id, optionalKeyRange, limit); + } else { + params = ObjectStoreGetAllParams(id, optionalKeyRange, limit); + } + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (aKeysOnly) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "getAllKeys(%s, %s)", + "IDBObjectStore.getAllKeys(%.0s%.0s%.0s%.0s%.0s)", + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "getAll(%s, %s)", + "IDBObjectStore.getAll(%.0s%.0s%.0s%.0s%.0s)", + 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)); + } + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mTransaction->InvalidateCursorCaches(); + + mTransaction->StartRequest(request, params); + + return request; +} + +RefPtr<IDBRequest> IDBObjectStore::Add(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + ValueWrapper valueWrapper(aCx, aValue); + + return AddOrPut(aCx, valueWrapper, aKey, false, /* aFromCursor */ false, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::Put(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + ValueWrapper valueWrapper(aCx, aValue); + + return AddOrPut(aCx, valueWrapper, aKey, true, /* aFromCursor */ false, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::Delete(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return DeleteInternal(aCx, aKey, /* aFromCursor */ false, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::Get(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ false, aCx, aKey, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::GetKey(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetInternal(/* aKeyOnly */ true, aCx, aKey, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::Clear(JSContext* aCx, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + if (!mTransaction->IsActive()) { + 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; + } + + const ObjectStoreClearParams params = {Id()}; + + auto request = GenerateRequest(aCx, this).unwrap(); + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).clear()", + "IDBObjectStore.clear(%.0s%.0s%.0s)", mTransaction->LoggingSerialNumber(), + request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this)); + + mTransaction->InvalidateCursorCaches(); + + mTransaction->StartRequest(request, params); + + return request; +} + +RefPtr<IDBRequest> IDBObjectStore::GetAll(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ false, aCx, aKey, aLimit, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::GetAllKeys(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return GetAllInternal(/* aKeysOnly */ true, aCx, aKey, aLimit, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::OpenCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ false, aCx, aRange, aDirection, + aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::OpenCursor(JSContext* aCx, + IDBCursorDirection aDirection, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ false, aCx, + JS::UndefinedHandleValue, aDirection, aRv); +} + +RefPtr<IDBRequest> IDBObjectStore::OpenKeyCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + return OpenCursorInternal(/* aKeysOnly */ true, aCx, aRange, aDirection, aRv); +} + +RefPtr<IDBIndex> IDBObjectStore::Index(const nsAString& aName, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (mTransaction->IsCommittingOrFinished() || mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + const nsTArray<IndexMetadata>& indexMetadatas = mSpec->indexes(); + + const auto endIndexMetadatas = indexMetadatas.cend(); + const auto foundMetadata = + std::find_if(indexMetadatas.cbegin(), endIndexMetadatas, + [&aName](const auto& indexMetadata) { + return indexMetadata.name() == aName; + }); + + if (foundMetadata == endIndexMetadatas) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return nullptr; + } + + const IndexMetadata& metadata = *foundMetadata; + + const auto endIndexes = mIndexes.cend(); + const auto foundIndex = + std::find_if(mIndexes.cbegin(), endIndexes, + [desiredId = metadata.id()](const auto& index) { + return index->Id() == desiredId; + }); + + RefPtr<IDBIndex> index; + + if (foundIndex == endIndexes) { + index = IDBIndex::Create(this, metadata); + MOZ_ASSERT(index); + + mIndexes.AppendElement(index); + } else { + index = *foundIndex; + } + + return index; +} + +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(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 IDBObjectStore_Binding::Wrap(aCx, this, aGivenProto); +} + +nsIGlobalObject* 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); +} + +RefPtr<DOMStringList> IDBObjectStore::IndexNames() { + AssertIsOnOwningThread(); + + return CreateSortedDOMStringList( + mSpec->indexes(), [](const auto& index) { return index.name(); }); +} + +RefPtr<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->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!keyRange) { + // Must specify a key or keyRange for get(). + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); + return nullptr; + } + + const int64_t id = Id(); + + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + const auto& params = + aKeyOnly ? RequestParams{ObjectStoreGetKeyParams(id, serializedKeyRange)} + : RequestParams{ObjectStoreGetParams(id, serializedKeyRange)}; + + auto request = GenerateRequest(aCx, this).unwrap(); + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).get(%s)", + "IDBObjectStore.get(%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mTransaction->InvalidateCursorCaches(); + + mTransaction->StartRequest(request, params); + + return request; +} + +RefPtr<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->IsActive()) { + 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; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + if (NS_WARN_IF((aRv.Failed()))) { + return nullptr; + } + + if (!keyRange) { + // Must specify a key or keyRange for delete(). + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); + return nullptr; + } + + ObjectStoreDeleteParams params; + params.objectStoreId() = Id(); + keyRange->ToSerialized(params.keyRange()); + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (!aFromCursor) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).delete(%s)", + "IDBObjectStore.delete(%.0s%.0s%.0s%.0s)", + 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); + + mTransaction->InvalidateCursorCaches(); + + return request; +} + +RefPtr<IDBIndex> IDBObjectStore::CreateIndex( + const nsAString& aName, const StringOrStringSequence& aKeyPath, + const IDBIndexParameters& aOptionalParameters, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (mTransaction->GetMode() != IDBTransaction::Mode::VersionChange || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return nullptr; + } + + const auto transaction = IDBTransaction::MaybeCurrent(); + if (!transaction || transaction != mTransaction || !transaction->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + const auto& indexes = mSpec->indexes(); + const auto end = indexes.cend(); + const auto foundIt = std::find_if( + indexes.cbegin(), end, + [&aName](const auto& index) { return aName == index.name(); }); + if (foundIt != end) { + aRv.ThrowConstraintError(nsPrintfCString( + "Index named '%s' already exists at index '%zu'", + NS_ConvertUTF16toUTF8(aName).get(), foundIt.GetIndex())); + return nullptr; + } + + const auto checkValid = [](const auto& keyPath) -> Result<KeyPath, nsresult> { + if (!keyPath.IsValid()) { + return Err(NS_ERROR_DOM_SYNTAX_ERR); + } + + return keyPath; + }; + + QM_INFOONLY_TRY_UNWRAP( + const auto maybeKeyPath, + ([&aKeyPath, checkValid]() -> Result<KeyPath, nsresult> { + if (aKeyPath.IsString()) { + QM_TRY_RETURN( + KeyPath::Parse(aKeyPath.GetAsString()).andThen(checkValid)); + } + + MOZ_ASSERT(aKeyPath.IsStringSequence()); + if (aKeyPath.GetAsStringSequence().IsEmpty()) { + return Err(NS_ERROR_DOM_SYNTAX_ERR); + } + + QM_TRY_RETURN( + KeyPath::Parse(aKeyPath.GetAsStringSequence()).andThen(checkValid)); + })()); + if (!maybeKeyPath) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return nullptr; + } + + const auto& keyPath = maybeKeyPath.ref(); + + if (aOptionalParameters.mMultiEntry && keyPath.IsArray()) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + +#ifdef DEBUG + { + const auto duplicateIndexName = std::any_of( + mIndexes.cbegin(), mIndexes.cend(), + [&aName](const auto& index) { return index->Name() == aName; }); + MOZ_ASSERT(!duplicateIndexName); + } +#endif + + const IndexMetadata* const 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"); + if (autoLocale) { + locale = IndexedDatabaseManager::GetLocale(); + } + + IndexMetadata* const metadata = mSpec->indexes().EmplaceBack( + 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); + + auto index = IDBIndex::Create(this, *metadata); + + 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).createIndex(%s)", + "IDBObjectStore.createIndex(%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(index)); + + return index; +} + +void IDBObjectStore::DeleteIndex(const nsAString& aName, ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (mTransaction->GetMode() != IDBTransaction::Mode::VersionChange || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR); + return; + } + + const auto transaction = IDBTransaction::MaybeCurrent(); + if (!transaction || transaction != mTransaction || !transaction->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return; + } + + const auto& metadataArray = mSpec->indexes(); + + const auto endMetadata = metadataArray.cend(); + const auto foundMetadataIt = std::find_if( + metadataArray.cbegin(), endMetadata, + [&aName](const auto& metadata) { return aName == metadata.name(); }); + + if (foundMetadataIt == endMetadata) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return; + } + + const auto foundId = foundMetadataIt->id(); + MOZ_ASSERT(foundId); + + // Must remove index from mIndexes before altering the metadata array! + { + const auto end = mIndexes.end(); + const auto foundIt = std::find_if( + mIndexes.begin(), end, + [foundId](const auto& index) { return index->Id() == foundId; }); + // TODO: Or should we assert foundIt != end? + if (foundIt != end) { + auto& index = *foundIt; + + index->NoteDeletion(); + + mDeletedIndexes.EmplaceBack(std::move(index)); + mIndexes.RemoveElementAt(foundIt.GetIndex()); + } + } + + mSpec->indexes().RemoveElementAt(foundMetadataIt.GetIndex()); + + RefreshSpec(/* aMayDelete */ false); + + // 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "deleteIndex(\"%s\")", + "IDBObjectStore.deleteIndex(%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), requestSerialNumber, + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), + NS_ConvertUTF16toUTF8(aName).get()); + + transaction->DeleteIndex(this, foundId); +} + +RefPtr<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->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aKey, &keyRange, aRv); + if (aRv.Failed()) { + return nullptr; + } + + ObjectStoreCountParams params; + params.objectStoreId() = Id(); + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + params.optionalKeyRange().emplace(serializedKeyRange); + } + + auto request = GenerateRequest(aCx, this).unwrap(); + + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).count(%s)", + "IDBObjectStore.count(%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange)); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mTransaction->InvalidateCursorCaches(); + + mTransaction->StartRequest(request, params); + + return request; +} + +RefPtr<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->IsActive()) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR); + return nullptr; + } + + RefPtr<IDBKeyRange> keyRange; + IDBKeyRange::FromJSVal(aCx, aRange, &keyRange, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + const int64_t objectStoreId = Id(); + + Maybe<SerializedKeyRange> optionalKeyRange; + + if (keyRange) { + SerializedKeyRange serializedKeyRange; + keyRange->ToSerialized(serializedKeyRange); + + optionalKeyRange.emplace(std::move(serializedKeyRange)); + } + + const CommonOpenCursorParams commonParams = { + objectStoreId, std::move(optionalKeyRange), aDirection}; + + // TODO: It would be great if the IPDL generator created a constructor + // accepting a CommonOpenCursorParams by value or rvalue reference. + const auto params = + aKeysOnly ? OpenCursorParams{ObjectStoreOpenKeyCursorParams{commonParams}} + : OpenCursorParams{ObjectStoreOpenCursorParams{commonParams}}; + + auto request = GenerateRequest(aCx, this).unwrap(); + + if (aKeysOnly) { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "openKeyCursor(%s, %s)", + "IDBObjectStore.openKeyCursor(%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aDirection)); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s)." + "openCursor(%s, %s)", + "IDBObjectStore.openCursor(%.0s%.0s%.0s%.0s%.0s)", + mTransaction->LoggingSerialNumber(), request->LoggingSerialNumber(), + IDB_LOG_STRINGIFY(mTransaction->Database()), + IDB_LOG_STRINGIFY(*mTransaction), IDB_LOG_STRINGIFY(this), + IDB_LOG_STRINGIFY(keyRange), IDB_LOG_STRINGIFY(aDirection)); + } + + const auto actor = + aKeysOnly + ? static_cast<SafeRefPtr<BackgroundCursorChildBase>>( + MakeSafeRefPtr< + BackgroundCursorChild<IDBCursorType::ObjectStoreKey>>( + request, this, aDirection)) + : MakeSafeRefPtr<BackgroundCursorChild<IDBCursorType::ObjectStore>>( + request, this, aDirection); + + // TODO: This is necessary to preserve request ordering only. Proper + // sequencing of requests should be done in a more sophisticated manner that + // doesn't require invalidating cursor caches (Bug 1580499). + mTransaction->InvalidateCursorCaches(); + + mTransaction->OpenCursor(*actor, params); + + return request; +} + +void IDBObjectStore::RefreshSpec(bool aMayDelete) { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(mDeletedSpec, mSpec == mDeletedSpec.get()); + + auto* const foundObjectStoreSpec = + mTransaction->Database()->LookupModifiableObjectStoreSpec( + [id = Id()](const auto& objSpec) { + return objSpec.metadata().id() == id; + }); + if (foundObjectStoreSpec) { + mSpec = foundObjectStoreSpec; + + for (auto& index : mIndexes) { + index->RefreshMetadata(aMayDelete); + } + + for (auto& index : mDeletedIndexes) { + index->RefreshMetadata(false); + } + } + + MOZ_ASSERT_IF(!aMayDelete && !mDeletedSpec, foundObjectStoreSpec); + + if (foundObjectStoreSpec) { + MOZ_ASSERT(mSpec != mDeletedSpec.get()); + 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.get() == mSpec); + return; + } + + // Copy the spec here. + mDeletedSpec = MakeUnique<ObjectStoreSpec>(*mSpec); + mDeletedSpec->indexes().Clear(); + + mSpec = mDeletedSpec.get(); + + for (const auto& 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::Mode::VersionChange || + mDeletedSpec) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + const auto transaction = IDBTransaction::MaybeCurrent(); + if (!transaction || transaction != mTransaction || !transaction->IsActive()) { + 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); + + const 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_CHILD_TRANSACTION_REQUEST( + "database(%s).transaction(%s).objectStore(%s).rename(%s)", + "IDBObjectStore.rename(%.0s%.0s%.0s%.0s)", + 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(); +} + +bool IDBObjectStore::ValueWrapper::Clone(JSContext* aCx) { + if (mCloned) { + return true; + } + + static const JSStructuredCloneCallbacks callbacks = { + CopyingStructuredCloneReadCallback /* read */, + CopyingStructuredCloneWriteCallback /* write */, + nullptr /* reportError */, + nullptr /* readTransfer */, + nullptr /* writeTransfer */, + nullptr /* freeTransfer */, + nullptr /* canTransfer */, + nullptr /* sabCloned */ + }; + + StructuredCloneInfo cloneInfo; + + JS::Rooted<JS::Value> clonedValue(aCx); + if (!JS_StructuredClone(aCx, mValue, &clonedValue, &callbacks, &cloneInfo)) { + return false; + } + + mValue = clonedValue; + + mCloned = true; + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBObjectStore.h b/dom/indexedDB/IDBObjectStore.h new file mode 100644 index 0000000000..dc79fa3616 --- /dev/null +++ b/dom/indexedDB/IDBObjectStore.h @@ -0,0 +1,296 @@ +/* -*- 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 "IDBCursor.h" +#include "js/RootingAPI.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IDBIndexBinding.h" +#include "mozilla/UniquePtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +struct JSClass; +class nsIGlobalObject; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class DOMStringList; +class IDBRequest; +class IDBTransaction; +class StringOrStringSequence; +template <typename> +class Sequence; + +namespace indexedDB { +class Key; +class KeyPath; +class IndexUpdateInfo; +class ObjectStoreSpec; +struct StructuredCloneReadInfoChild; +} // namespace indexedDB + +class IDBObjectStore final : public nsISupports, public nsWrapperCache { + using IndexUpdateInfo = indexedDB::IndexUpdateInfo; + using Key = indexedDB::Key; + using KeyPath = indexedDB::KeyPath; + using ObjectStoreSpec = indexedDB::ObjectStoreSpec; + using StructuredCloneReadInfoChild = indexedDB::StructuredCloneReadInfoChild; + + // For AddOrPut() and DeleteInternal(). + // TODO Consider removing this, and making the functions public? + template <IDBCursor::Type> + friend class IDBTypedCursor; + + static const JSClass sDummyPropJSClass; + + // TODO: This could be made const if Bug 1575173 is resolved. It is + // initialized in the constructor and never modified/cleared. + SafeRefPtr<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. + ObjectStoreSpec* mSpec; + UniquePtr<ObjectStoreSpec> mDeletedSpec; + + nsTArray<RefPtr<IDBIndex>> mIndexes; + nsTArray<RefPtr<IDBIndex>> mDeletedIndexes; + + const int64_t mId; + bool mRooted; + + public: + struct StructuredCloneWriteInfo; + struct StructuredCloneInfo; + + class MOZ_STACK_CLASS ValueWrapper final { + JS::Rooted<JS::Value> mValue; + bool mCloned; + + public: + ValueWrapper(JSContext* aCx, JS::Handle<JS::Value> aValue) + : mValue(aCx, aValue), mCloned(false) { + MOZ_COUNT_CTOR(IDBObjectStore::ValueWrapper); + } + + MOZ_COUNTED_DTOR_NESTED(ValueWrapper, IDBObjectStore::ValueWrapper) + + const JS::Rooted<JS::Value>& Value() const { return mValue; } + + bool Clone(JSContext* aCx); + }; + + [[nodiscard]] static RefPtr<IDBObjectStore> Create( + SafeRefPtr<IDBTransaction> aTransaction, ObjectStoreSpec& aSpec); + + static void AppendIndexUpdateInfo(int64_t aIndexID, const KeyPath& aKeyPath, + bool aMultiEntry, const nsCString& aLocale, + JSContext* aCx, JS::Handle<JS::Value> aVal, + nsTArray<IndexUpdateInfo>* aUpdateInfoArray, + ErrorResult* aRv); + + static void ClearCloneReadInfo( + indexedDB::StructuredCloneReadInfoChild& aReadInfo); + + static bool DeserializeValue(JSContext* aCx, + StructuredCloneReadInfoChild&& aCloneReadInfo, + JS::MutableHandle<JS::Value> aValue); + + 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; + + nsIGlobalObject* 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); + + [[nodiscard]] RefPtr<DOMStringList> IndexNames(); + + const IDBTransaction& TransactionRef() const { + AssertIsOnOwningThread(); + + return *mTransaction; + } + + IDBTransaction& MutableTransactionRef() { + AssertIsOnOwningThread(); + + return *mTransaction; + } + + SafeRefPtr<IDBTransaction> AcquireTransaction() const { + AssertIsOnOwningThread(); + + return mTransaction.clonePtr(); + } + + RefPtr<IDBTransaction> Transaction() const { + AssertIsOnOwningThread(); + + return AsRefPtr(mTransaction.clonePtr()); + } + + [[nodiscard]] RefPtr<IDBRequest> Add(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Put(JSContext* aCx, + JS::Handle<JS::Value> aValue, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Delete(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Get(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetKey(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Clear(JSContext* aCx, ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBIndex> CreateIndex( + const nsAString& aName, const StringOrStringSequence& aKeyPath, + const IDBIndexParameters& aOptionalParameters, ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBIndex> Index(const nsAString& aName, + ErrorResult& aRv); + + void DeleteIndex(const nsAString& aName, ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> Count(JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetAll(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetAllKeys(JSContext* aCx, + JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> OpenCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> OpenCursor(JSContext* aCx, + IDBCursorDirection aDirection, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> OpenKeyCursor(JSContext* aCx, + JS::Handle<JS::Value> aRange, + IDBCursorDirection aDirection, + ErrorResult& 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(SafeRefPtr<IDBTransaction> aTransaction, + ObjectStoreSpec* aSpec); + + ~IDBObjectStore(); + + void GetAddInfo(JSContext* aCx, ValueWrapper& aValueWrapper, + JS::Handle<JS::Value> aKeyVal, + StructuredCloneWriteInfo& aCloneWriteInfo, Key& aKey, + nsTArray<IndexUpdateInfo>& aUpdateInfoArray, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> AddOrPut(JSContext* aCx, + ValueWrapper& aValueWrapper, + JS::Handle<JS::Value> aKey, + bool aOverwrite, bool aFromCursor, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> DeleteInternal(JSContext* aCx, + JS::Handle<JS::Value> aKey, + bool aFromCursor, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetInternal(bool aKeyOnly, JSContext* aCx, + JS::Handle<JS::Value> aKey, + ErrorResult& aRv); + + [[nodiscard]] RefPtr<IDBRequest> GetAllInternal( + bool aKeysOnly, JSContext* aCx, JS::Handle<JS::Value> aKey, + const Optional<uint32_t>& aLimit, ErrorResult& aRv); + + [[nodiscard]] RefPtr<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 0000000000..efc3f9a44e --- /dev/null +++ b/dom/indexedDB/IDBRequest.cpp @@ -0,0 +1,445 @@ +/* -*- 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 <utility> + +#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 "ReportInternalError.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/IDBOpenDBRequestBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIGlobalObject.h" +#include "nsIScriptContext.h" +#include "nsJSUtils.h" +#include "nsString.h" +#include "ThreadLocal.h" + +namespace mozilla::dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::ipc; + +IDBRequest::IDBRequest(IDBDatabase* aDatabase) + : DOMEventTargetHelper(aDatabase), + mLoggingSerialNumber(0), + mLineNo(0), + mColumn(0), + mHaveResultOrErrorCode(false) { + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + InitMembers(); +} + +IDBRequest::IDBRequest(nsIGlobalObject* aGlobal) + : DOMEventTargetHelper(aGlobal), + mLoggingSerialNumber(0), + mLineNo(0), + mColumn(0), + mHaveResultOrErrorCode(false) { + InitMembers(); +} + +IDBRequest::~IDBRequest() { + AssertIsOnOwningThread(); + mozilla::DropJSObjects(this); +} + +void IDBRequest::InitMembers() { + AssertIsOnOwningThread(); + + mResultVal.setUndefined(); + mLoggingSerialNumber = NextSerialNumber(); + mErrorCode = NS_OK; + mLineNo = 0; + mColumn = 0; + mHaveResultOrErrorCode = false; +} + +// static +MovingNotNull<RefPtr<IDBRequest>> IDBRequest::Create( + JSContext* aCx, IDBDatabase* aDatabase, + SafeRefPtr<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 = std::move(aTransaction); + + return WrapMovingNotNullUnchecked(std::move(request)); +} + +// static +MovingNotNull<RefPtr<IDBRequest>> IDBRequest::Create( + JSContext* aCx, IDBObjectStore* aSourceAsObjectStore, + IDBDatabase* aDatabase, SafeRefPtr<IDBTransaction> aTransaction) { + MOZ_ASSERT(aSourceAsObjectStore); + aSourceAsObjectStore->AssertIsOnOwningThread(); + + auto request = + Create(aCx, aDatabase, std::move(aTransaction)).unwrapBasePtr(); + + request->mSourceAsObjectStore = aSourceAsObjectStore; + + return WrapMovingNotNullUnchecked(std::move(request)); +} + +// static +MovingNotNull<RefPtr<IDBRequest>> IDBRequest::Create( + JSContext* aCx, IDBIndex* aSourceAsIndex, IDBDatabase* aDatabase, + SafeRefPtr<IDBTransaction> aTransaction) { + MOZ_ASSERT(aSourceAsIndex); + aSourceAsIndex->AssertIsOnOwningThread(); + + auto request = + Create(aCx, aDatabase, std::move(aTransaction)).unwrapBasePtr(); + + request->mSourceAsIndex = aSourceAsIndex; + + return WrapMovingNotNullUnchecked(std::move(request)); +} + +// static +uint64_t IDBRequest::NextSerialNumber() { + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + const auto& 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::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 = DOMException::Create(aRv); + mErrorCode = aRv; + + mResultVal.setUndefined(); +} + +#ifdef DEBUG + +nsresult IDBRequest::GetErrorCode() const { + AssertIsOnOwningThread(); + MOZ_ASSERT(mHaveResultOrErrorCode); + + return mErrorCode; +} + +DOMException* 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 IDBRequest_Binding::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); +} + +DOMException* 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, + DOMEventTargetHelper) + 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, + DOMEventTargetHelper) + mozilla::DropJSObjects(tmp); + 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, 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(mResultVal) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBRequest) + if (aIID.Equals(NS_GET_IID(mozilla::dom::detail::PrivateIDBRequest))) { + foundInterface = static_cast<EventTarget*>(this); + } else +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(IDBRequest, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(IDBRequest, DOMEventTargetHelper) + +void IDBRequest::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + AssertIsOnOwningThread(); + + aVisitor.mCanHandle = true; + aVisitor.SetParentTarget(mTransaction.unsafeGetRawPtr(), false); +} + +IDBOpenDBRequest::IDBOpenDBRequest(SafeRefPtr<IDBFactory> aFactory, + nsIGlobalObject* aGlobal) + : IDBRequest(aGlobal), + mFactory(std::move(aFactory)), + mIncreasedActiveDatabaseCount(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mFactory); + MOZ_ASSERT(aGlobal); +} + +IDBOpenDBRequest::~IDBOpenDBRequest() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mIncreasedActiveDatabaseCount); +} + +// static +RefPtr<IDBOpenDBRequest> IDBOpenDBRequest::Create( + JSContext* aCx, SafeRefPtr<IDBFactory> aFactory, nsIGlobalObject* aGlobal) { + MOZ_ASSERT(aFactory); + aFactory->AssertIsOnOwningThread(); + MOZ_ASSERT(aGlobal); + + RefPtr<IDBOpenDBRequest> request = + new IDBOpenDBRequest(std::move(aFactory), aGlobal); + CaptureCaller(aCx, request->mFilename, &request->mLineNo, &request->mColumn); + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + workerPrivate->AssertIsOnWorkerThread(); + + request->mWorkerRef = + StrongWorkerRef::Create(workerPrivate, "IDBOpenDBRequest"); + if (NS_WARN_IF(!request->mWorkerRef)) { + return nullptr; + } + } + + request->IncreaseActiveDatabaseCount(); + + return request; +} + +void IDBOpenDBRequest::SetTransaction(SafeRefPtr<IDBTransaction> aTransaction) { + AssertIsOnOwningThread(); + + MOZ_ASSERT(!aTransaction || !mTransaction); + + mTransaction = std::move(aTransaction); +} + +void IDBOpenDBRequest::DispatchNonTransactionError(nsresult aErrorCode) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aErrorCode)); + MOZ_ASSERT(NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_DOM_INDEXEDDB); + + // The actor failed to initiate, decrease the number of active IDBOpenRequests + // here since NoteComplete won't be called. + MaybeDecreaseActiveDatabaseCount(); + + SetError(aErrorCode); + + // Make an error event and fire it at the target. + auto event = CreateGenericEvent(this, nsDependentString(kErrorEventType), + eDoesBubble, eCancelable); + + IgnoredErrorResult rv; + DispatchEvent(*event, rv); + if (rv.Failed()) { + NS_WARNING("Failed to dispatch event!"); + } +} + +void IDBOpenDBRequest::NoteComplete() { + AssertIsOnOwningThread(); + MOZ_ASSERT_IF(!NS_IsMainThread(), mWorkerRef); + + // Normally, we decrease the number of active IDBOpenRequests here. + MaybeDecreaseActiveDatabaseCount(); + + // If we have a WorkerRef, then nulling this out will release the worker. + mWorkerRef = nullptr; +} + +void IDBOpenDBRequest::IncreaseActiveDatabaseCount() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mIncreasedActiveDatabaseCount); + + // Increase the number of active IDBOpenRequests. + // Note: We count here instead of the actor's ctor because the preemption + // could happen at next JS interrupt but its BackgroundFactoryRequestChild + // could be created asynchronously from IDBFactory::BackgroundCreateCallback + // ::ActorCreated() if its PBackgroundChild is not created yet on this thread. + mFactory->UpdateActiveDatabaseCount(1); + mIncreasedActiveDatabaseCount = true; +} + +void IDBOpenDBRequest::MaybeDecreaseActiveDatabaseCount() { + AssertIsOnOwningThread(); + + if (mIncreasedActiveDatabaseCount) { + // Decrease the number of active IDBOpenRequests. + mFactory->UpdateActiveDatabaseCount(-1); + mIncreasedActiveDatabaseCount = false; + } +} + +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(IDBOpenDBRequest) +NS_INTERFACE_MAP_END_INHERITING(IDBRequest) + +NS_IMPL_ADDREF_INHERITED(IDBOpenDBRequest, IDBRequest) +NS_IMPL_RELEASE_INHERITED(IDBOpenDBRequest, IDBRequest) + +JSObject* IDBOpenDBRequest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + AssertIsOnOwningThread(); + + return IDBOpenDBRequest_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBRequest.h b/dom/indexedDB/IDBRequest.h new file mode 100644 index 0000000000..4bbb7eefa7 --- /dev/null +++ b/dom/indexedDB/IDBRequest.h @@ -0,0 +1,294 @@ +/* -*- 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/DOMException.h" +#include "mozilla/dom/IDBRequestBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsCycleCollectionParticipant.h" +#include "ReportInternalError.h" +#include "SafeRefPtr.h" + +#define PRIVATE_IDBREQUEST_IID \ + { \ + 0xe68901e5, 0x1d50, 0x4ee9, { \ + 0xaf, 0x49, 0x90, 0x99, 0x4a, 0xff, 0xc8, 0x39 \ + } \ + } + +class nsIGlobalObject; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +class IDBCursor; +class IDBDatabase; +class IDBFactory; +class IDBIndex; +class IDBObjectStore; +class IDBTransaction; +template <typename> +struct Nullable; +class OwningIDBObjectStoreOrIDBIndexOrIDBCursor; +class StrongWorkerRef; + +namespace detail { +// This class holds the IID for use with NS_GET_IID. +class PrivateIDBRequest { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(PRIVATE_IDBREQUEST_IID) +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(PrivateIDBRequest, PRIVATE_IDBREQUEST_IID) + +} // namespace detail + +class IDBRequest : public DOMEventTargetHelper { + 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; + + SafeRefPtr<IDBTransaction> mTransaction; + + JS::Heap<JS::Value> mResultVal; + RefPtr<DOMException> mError; + + nsString mFilename; + uint64_t mLoggingSerialNumber; + nsresult mErrorCode; + uint32_t mLineNo; + uint32_t mColumn; + bool mHaveResultOrErrorCode; + + public: + [[nodiscard]] static MovingNotNull<RefPtr<IDBRequest>> Create( + JSContext* aCx, IDBDatabase* aDatabase, + SafeRefPtr<IDBTransaction> aTransaction); + + [[nodiscard]] static MovingNotNull<RefPtr<IDBRequest>> Create( + JSContext* aCx, IDBObjectStore* aSource, IDBDatabase* aDatabase, + SafeRefPtr<IDBTransaction> aTransaction); + + [[nodiscard]] static MovingNotNull<RefPtr<IDBRequest>> Create( + JSContext* aCx, IDBIndex* aSource, IDBDatabase* aDatabase, + SafeRefPtr<IDBTransaction> aTransaction); + + static void CaptureCaller(JSContext* aCx, nsAString& aFilename, + uint32_t* aLineNo, uint32_t* aColumn); + + static uint64_t NextSerialNumber(); + + // EventTarget + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + void GetSource( + Nullable<OwningIDBObjectStoreOrIDBIndexOrIDBCursor>& aSource) const; + + void Reset(); + + template <typename ResultCallback> + void SetResult(const ResultCallback& aCallback) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mHaveResultOrErrorCode); + MOZ_ASSERT(mResultVal.isUndefined()); + MOZ_ASSERT(!mError); + + // Already disconnected from the owner. + if (!GetOwnerGlobal()) { + SetError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + // See this global is still valid. + if (NS_WARN_IF(NS_FAILED(CheckCurrentGlobalCorrectness()))) { + SetError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + AutoJSAPI autoJS; + if (!autoJS.Init(GetOwnerGlobal())) { + IDB_WARNING("Failed to initialize AutoJSAPI!"); + SetError(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return; + } + + JSContext* cx = autoJS.cx(); + + JS::Rooted<JS::Value> result(cx); + nsresult rv = aCallback(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; + mozilla::HoldJSObjects(this); + + mHaveResultOrErrorCode = true; + } + + void SetError(nsresult aRv); + + nsresult GetErrorCode() const +#ifdef DEBUG + ; +#else + { + return mErrorCode; + } +#endif + + DOMException* GetErrorAfterResult() const +#ifdef DEBUG + ; +#else + { + return mError; + } +#endif + + DOMException* 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); + + nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); } + + 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); + } + + Maybe<IDBTransaction&> MaybeTransactionRef() const { + AssertIsOnOwningThread(); + + return mTransaction.maybeDeref(); + } + + IDBTransaction& MutableTransactionRef() const { + AssertIsOnOwningThread(); + + return *mTransaction; + } + + SafeRefPtr<IDBTransaction> AcquireTransaction() const { + AssertIsOnOwningThread(); + + return mTransaction.clonePtr(); + } + + // For WebIDL binding. + RefPtr<IDBTransaction> GetTransaction() const { + AssertIsOnOwningThread(); + + return AsRefPtr(mTransaction.clonePtr()); + } + + IDBRequestReadyState ReadyState() const; + + void SetSource(IDBCursor* aSource); + + IMPL_EVENT_HANDLER(success); + IMPL_EVENT_HANDLER(error); + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(IDBRequest); } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(IDBRequest, + DOMEventTargetHelper) + + // nsWrapperCache + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + explicit IDBRequest(IDBDatabase* aDatabase); + explicit IDBRequest(nsIGlobalObject* aGlobal); + ~IDBRequest(); + + void InitMembers(); + + void ConstructResult(); +}; + +class IDBOpenDBRequest final : public IDBRequest { + // Only touched on the owning thread. + SafeRefPtr<IDBFactory> mFactory; + + RefPtr<StrongWorkerRef> mWorkerRef; + + bool mIncreasedActiveDatabaseCount; + + public: + [[nodiscard]] static RefPtr<IDBOpenDBRequest> Create( + JSContext* aCx, SafeRefPtr<IDBFactory> aFactory, + nsIGlobalObject* aGlobal); + + void SetTransaction(SafeRefPtr<IDBTransaction> aTransaction); + + void DispatchNonTransactionError(nsresult aErrorCode); + + void NoteComplete(); + + // EventTarget + 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(SafeRefPtr<IDBFactory> aFactory, nsIGlobalObject* aGlobal); + + ~IDBOpenDBRequest(); + + void IncreaseActiveDatabaseCount(); + + void MaybeDecreaseActiveDatabaseCount(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbrequest_h__ diff --git a/dom/indexedDB/IDBResult.h b/dom/indexedDB/IDBResult.h new file mode 100644 index 0000000000..424eda9bff --- /dev/null +++ b/dom/indexedDB/IDBResult.h @@ -0,0 +1,158 @@ +/* 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_idbresult_h__ +#define mozilla_dom_indexeddb_idbresult_h__ + +#include "mozilla/ErrorResult.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Variant.h" + +#include <type_traits> +#include <utility> + +namespace mozilla::dom::indexedDB { + +// IDBSpecialValue represents two special return values, distinct from any other +// value, used in several places in the IndexedDB spec. +enum class IDBSpecialValue { + Failure, + Invalid, +}; + +namespace detail { + +template <IDBSpecialValue Value> +using SpecialConstant = std::integral_constant<IDBSpecialValue, Value>; +using FailureType = SpecialConstant<IDBSpecialValue::Failure>; +using InvalidType = SpecialConstant<IDBSpecialValue::Invalid>; +struct ExceptionType final {}; +} // namespace detail + +// Put these in a subnamespace to avoid conflicts from the combination of 1. +// using namespace mozilla::dom::indexedDB; in cpp files, 2. the unified build +// and 3. mozilla::dom::Exception +namespace SpecialValues { +constexpr const detail::FailureType Failure; +constexpr const detail::InvalidType Invalid; +constexpr const detail::ExceptionType Exception; +} // namespace SpecialValues + +namespace detail { +template <IDBSpecialValue... Elements> +struct IsSortedSet; + +template <IDBSpecialValue First, IDBSpecialValue Second, + IDBSpecialValue... Rest> +struct IsSortedSet<First, Second, Rest...> + : std::integral_constant<bool, IsSortedSet<First, Second>::value && + IsSortedSet<Second, Rest...>::value> {}; + +template <IDBSpecialValue First, IDBSpecialValue Second> +struct IsSortedSet<First, Second> + : std::integral_constant<bool, (First < Second)> {}; + +template <IDBSpecialValue First> +struct IsSortedSet<First> : std::true_type {}; + +template <> +struct IsSortedSet<> : std::true_type {}; + +template <IDBSpecialValue... S> +class IDBError { + // This assertion ensures that permutations of the set of possible special + // values don't create distinct types. + static_assert(IsSortedSet<S...>::value, + "special value list must be sorted and unique"); + + template <IDBSpecialValue... U> + friend class IDBError; + + public: + MOZ_IMPLICIT IDBError(nsresult aRv) : mVariant(ErrorResult{aRv}) {} + + IDBError(ExceptionType, ErrorResult&& aErrorResult) + : mVariant(std::move(aErrorResult)) {} + + template <IDBSpecialValue Special> + MOZ_IMPLICIT IDBError(SpecialConstant<Special>) + : mVariant(SpecialConstant<Special>{}) {} + + IDBError(IDBError&&) = default; + IDBError& operator=(IDBError&&) = default; + + // Construct an IDBResult from another IDBResult whose set of possible special + // values is a subset of this one's. + template <IDBSpecialValue... U> + MOZ_IMPLICIT IDBError(IDBError<U...>&& aOther) + : mVariant(aOther.mVariant.match( + [](auto& aVariant) { return VariantType{std::move(aVariant)}; })) {} + + bool Is(ExceptionType) const { return mVariant.template is<ErrorResult>(); } + + template <IDBSpecialValue Special> + bool Is(SpecialConstant<Special>) const { + return mVariant.template is<SpecialConstant<Special>>(); + } + + ErrorResult& AsException() { return mVariant.template as<ErrorResult>(); } + + template <typename... SpecialValueMappers> + ErrorResult ExtractErrorResult(SpecialValueMappers... aSpecialValueMappers) { +#if defined(__clang__) || (defined(__GNUC__) && __GNUC__ >= 8) + return mVariant.match( + [](ErrorResult& aException) { return std::move(aException); }, + [aSpecialValueMappers](const SpecialConstant<S>& aSpecialValue) { + return ErrorResult{aSpecialValueMappers(aSpecialValue)}; + }...); +#else + // gcc 7 doesn't accept the kind of parameter pack expansion above, + // probably due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47226 + return mVariant.match([aSpecialValueMappers...](auto& aValue) { + if constexpr (std::is_same_v<ErrorResult&, decltype(aValue)>) { + return std::move(aValue); + } else { + return ErrorResult{aSpecialValueMappers(aValue)...}; + } + }); +#endif + } + + template <typename... SpecialValueMappers> + nsresult ExtractNSResult(SpecialValueMappers... aSpecialValueMappers) { + return mVariant.match( + [](ErrorResult& aException) { return aException.StealNSResult(); }, + aSpecialValueMappers...); + } + + protected: + using VariantType = Variant<ErrorResult, SpecialConstant<S>...>; + + VariantType mVariant; +}; +} // namespace detail + +// Represents a return value of an IndexedDB algorithm. T is the type of the +// regular return value, while S is a list of special values that can be +// returned by the particular algorithm. +template <typename T, IDBSpecialValue... S> +using IDBResult = Result<T, detail::IDBError<S...>>; + +template <nsresult E> +nsresult InvalidMapsTo(const indexedDB::detail::InvalidType&) { + return E; +} + +inline detail::IDBError<> IDBException(nsresult aRv) { + return {SpecialValues::Exception, ErrorResult{aRv}}; +} + +template <IDBSpecialValue Special> +detail::IDBError<Special> IDBError(detail::SpecialConstant<Special> aResult) { + return {aResult}; +} + +} // namespace mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_idbresult_h__ diff --git a/dom/indexedDB/IDBTransaction.cpp b/dom/indexedDB/IDBTransaction.cpp new file mode 100644 index 0000000000..d984bcacdf --- /dev/null +++ b/dom/indexedDB/IDBTransaction.cpp @@ -0,0 +1,1019 @@ +/* -*- 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/HoldDropJSObjects.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ScopeExit.h" +#include "nsPIDOMWindow.h" +#include "nsQueryObject.h" +#include "nsServiceManagerUtils.h" +#include "nsTHashtable.h" +#include "ProfilerHelpers.h" +#include "ReportInternalError.h" +#include "ThreadLocal.h" + +// Include this last to avoid path problems on Windows. +#include "ActorsChild.h" + +namespace { +using namespace mozilla::dom::indexedDB; +using namespace mozilla::ipc; + +// TODO: Move this to xpcom/ds. +template <typename T, typename Range, typename Transformation> +nsTHashtable<T> TransformToHashtable(const Range& aRange, + const Transformation& aTransformation) { + // TODO: Determining the size of the range is not syntactically necessary (and + // requires random access iterators if expressed this way). It is a + // performance optimization. We could resort to std::distance to support any + // iterator category, but this would lead to a double iteration of the range + // in case of non-random-access iterators. It is hard to determine in general + // if double iteration or reallocation is worse. + auto res = nsTHashtable<T>(aRange.cend() - aRange.cbegin()); + // TOOD: std::transform could be used if nsTHashtable had an insert_iterator, + // and this would also allow a more generic version not depending on + // nsTHashtable at all. + for (const auto& item : aRange) { + res.PutEntry(aTransformation(item)); + } + return res; +} + +ThreadLocal* GetIndexedDBThreadLocal() { + BackgroundChildImpl::ThreadLocal* const threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + MOZ_ASSERT(threadLocal); + + ThreadLocal* idbThreadLocal = threadLocal->mIndexedDBThreadLocal.get(); + MOZ_ASSERT(idbThreadLocal); + + return idbThreadLocal; +} +} // namespace + +namespace mozilla::dom { + +using namespace mozilla::dom::indexedDB; +using namespace mozilla::ipc; + +bool IDBTransaction::HasTransactionChild() const { + return (mMode == Mode::VersionChange + ? static_cast<void*>( + mBackgroundActor.mVersionChangeBackgroundActor) + : mBackgroundActor.mNormalBackgroundActor) != nullptr; +} + +template <typename Func> +auto IDBTransaction::DoWithTransactionChild(const Func& aFunc) const { + MOZ_ASSERT(HasTransactionChild()); + return mMode == Mode::VersionChange + ? aFunc(*mBackgroundActor.mVersionChangeBackgroundActor) + : aFunc(*mBackgroundActor.mNormalBackgroundActor); +} + +IDBTransaction::IDBTransaction(IDBDatabase* const aDatabase, + const nsTArray<nsString>& aObjectStoreNames, + const Mode aMode, nsString aFilename, + const uint32_t aLineNo, const uint32_t aColumn, + CreatedFromFactoryFunction /*aDummy*/) + : DOMEventTargetHelper(aDatabase), + mDatabase(aDatabase), + mObjectStoreNames(aObjectStoreNames.Clone()), + mLoggingSerialNumber(GetIndexedDBThreadLocal()->NextTransactionSN(aMode)), + mNextObjectStoreId(0), + mNextIndexId(0), + mAbortCode(NS_OK), + mPendingRequestCount(0), + mFilename(std::move(aFilename)), + mLineNo(aLineNo), + mColumn(aColumn), + mMode(aMode), + mRegistered(false), + mNotedActiveTransaction(false) { + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + + // This also nulls mBackgroundActor.mVersionChangeBackgroundActor, so this is + // valid also for mMode == Mode::VersionChange. + mBackgroundActor.mNormalBackgroundActor = nullptr; + +#ifdef DEBUG + if (!aObjectStoreNames.IsEmpty()) { + // Make sure the array is properly sorted. + MOZ_ASSERT( + std::is_sorted(aObjectStoreNames.cbegin(), aObjectStoreNames.cend())); + + // Make sure there are no duplicates in our objectStore names. + MOZ_ASSERT(aObjectStoreNames.cend() == + std::adjacent_find(aObjectStoreNames.cbegin(), + aObjectStoreNames.cend())); + } +#endif + + mozilla::HoldJSObjects(this); +} + +IDBTransaction::~IDBTransaction() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mPendingRequestCount); + MOZ_ASSERT(mReadyState != ReadyState::Active); + MOZ_ASSERT(mReadyState != ReadyState::Inactive); + MOZ_ASSERT(mReadyState != ReadyState::Committing); + MOZ_ASSERT(!mNotedActiveTransaction); + MOZ_ASSERT(mSentCommitOrAbort); + MOZ_ASSERT_IF(HasTransactionChild(), mFiredCompleteOrAbort); + + if (mRegistered) { + mDatabase->UnregisterTransaction(*this); +#ifdef DEBUG + mRegistered = false; +#endif + } + + if (HasTransactionChild()) { + if (mMode == Mode::VersionChange) { + mBackgroundActor.mVersionChangeBackgroundActor->SendDeleteMeInternal( + /* aFailedConstructor */ false); + } else { + mBackgroundActor.mNormalBackgroundActor->SendDeleteMeInternal(); + } + } + MOZ_ASSERT(!HasTransactionChild(), + "SendDeleteMeInternal should have cleared!"); + + mozilla::DropJSObjects(this); +} + +// static +SafeRefPtr<IDBTransaction> IDBTransaction::CreateVersionChange( + IDBDatabase* const aDatabase, + BackgroundVersionChangeTransactionChild* const aActor, + const NotNull<IDBOpenDBRequest*> aOpenRequest, + const int64_t aNextObjectStoreId, const int64_t aNextIndexId) { + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aNextObjectStoreId > 0); + MOZ_ASSERT(aNextIndexId > 0); + + const nsTArray<nsString> emptyObjectStoreNames; + + nsString filename; + uint32_t lineNo, column; + aOpenRequest->GetCallerLocation(filename, &lineNo, &column); + auto transaction = MakeSafeRefPtr<IDBTransaction>( + aDatabase, emptyObjectStoreNames, Mode::VersionChange, + std::move(filename), lineNo, column, CreatedFromFactoryFunction{}); + + transaction->NoteActiveTransaction(); + + transaction->mBackgroundActor.mVersionChangeBackgroundActor = aActor; + transaction->mNextObjectStoreId = aNextObjectStoreId; + transaction->mNextIndexId = aNextIndexId; + + aDatabase->RegisterTransaction(*transaction); + transaction->mRegistered = true; + + return transaction; +} + +// static +SafeRefPtr<IDBTransaction> IDBTransaction::Create( + JSContext* const aCx, IDBDatabase* const aDatabase, + const nsTArray<nsString>& aObjectStoreNames, const Mode aMode) { + MOZ_ASSERT(aDatabase); + aDatabase->AssertIsOnOwningThread(); + MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); + MOZ_ASSERT(aMode == Mode::ReadOnly || aMode == Mode::ReadWrite || + aMode == Mode::ReadWriteFlush || aMode == Mode::Cleanup); + + nsString filename; + uint32_t lineNo, column; + IDBRequest::CaptureCaller(aCx, filename, &lineNo, &column); + auto transaction = MakeSafeRefPtr<IDBTransaction>( + aDatabase, aObjectStoreNames, aMode, std::move(filename), lineNo, column, + CreatedFromFactoryFunction{}); + + if (!NS_IsMainThread()) { + WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + workerPrivate->AssertIsOnWorkerThread(); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + workerPrivate, "IDBTransaction", + [transaction = AsRefPtr(transaction.clonePtr())]() { + transaction->AssertIsOnOwningThread(); + if (!transaction->IsCommittingOrFinished()) { + IDB_REPORT_INTERNAL_ERR(); + transaction->AbortInternal(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + nullptr); + } + }); + if (NS_WARN_IF(!workerRef)) { +#ifdef DEBUG + // Silence the destructor assertions if we never made this object live. + transaction->mReadyState = ReadyState::Finished; + transaction->mSentCommitOrAbort.Flip(); +#endif + return nullptr; + } + + transaction->mWorkerRef = std::move(workerRef); + } + + nsCOMPtr<nsIRunnable> runnable = + do_QueryObject(transaction.unsafeGetRawPtr()); + nsContentUtils::AddPendingIDBTransaction(runnable.forget()); + + aDatabase->RegisterTransaction(*transaction); + transaction->mRegistered = true; + + return transaction; +} + +// static +Maybe<IDBTransaction&> IDBTransaction::MaybeCurrent() { + using namespace mozilla::ipc; + + MOZ_ASSERT(BackgroundChild::GetForCurrentThread()); + + return GetIndexedDBThreadLocal()->MaybeCurrentTransactionRef(); +} + +#ifdef DEBUG + +void IDBTransaction::AssertIsOnOwningThread() const { + MOZ_ASSERT(mDatabase); + mDatabase->AssertIsOnOwningThread(); +} + +#endif // DEBUG + +void IDBTransaction::SetBackgroundActor( + indexedDB::BackgroundTransactionChild* const aBackgroundActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aBackgroundActor); + MOZ_ASSERT(!mBackgroundActor.mNormalBackgroundActor); + MOZ_ASSERT(mMode != Mode::VersionChange); + + NoteActiveTransaction(); + + mBackgroundActor.mNormalBackgroundActor = aBackgroundActor; +} + +BackgroundRequestChild* IDBTransaction::StartRequest( + MovingNotNull<RefPtr<mozilla::dom::IDBRequest> > aRequest, + const RequestParams& aParams) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + BackgroundRequestChild* const actor = + new BackgroundRequestChild(std::move(aRequest)); + + DoWithTransactionChild([actor, &aParams](auto& transactionChild) { + transactionChild.SendPBackgroundIDBRequestConstructor(actor, aParams); + }); + + // Balanced in BackgroundRequestChild::Recv__delete__(). + OnNewRequest(); + + return actor; +} + +void IDBTransaction::OpenCursor(PBackgroundIDBCursorChild& aBackgroundActor, + const OpenCursorParams& aParams) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); + + DoWithTransactionChild([&aBackgroundActor, &aParams](auto& actor) { + actor.SendPBackgroundIDBCursorConstructor(&aBackgroundActor, aParams); + }); + + // Balanced in BackgroundCursorChild::RecvResponse(). + OnNewRequest(); +} + +void IDBTransaction::RefreshSpec(const bool aMayDelete) { + AssertIsOnOwningThread(); + + for (auto& objectStore : mObjectStores) { + objectStore->RefreshSpec(aMayDelete); + } + + for (auto& objectStore : mDeletedObjectStores) { + objectStore->RefreshSpec(false); + } +} + +void IDBTransaction::OnNewRequest() { + AssertIsOnOwningThread(); + + if (!mPendingRequestCount) { + MOZ_ASSERT(ReadyState::Active == mReadyState); + mStarted.Flip(); + } + + ++mPendingRequestCount; +} + +void IDBTransaction::OnRequestFinished( + const bool aRequestCompletedSuccessfully) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mReadyState != ReadyState::Active); + MOZ_ASSERT_IF(mReadyState == ReadyState::Finished, !NS_SUCCEEDED(mAbortCode)); + MOZ_ASSERT(mPendingRequestCount); + + --mPendingRequestCount; + + if (!mPendingRequestCount) { + if (mSentCommitOrAbort) { + return; + } + + if (aRequestCompletedSuccessfully) { + if (mReadyState == ReadyState::Inactive) { + mReadyState = ReadyState::Committing; + } + + if (NS_SUCCEEDED(mAbortCode)) { + SendCommit(true); + } else { + SendAbort(mAbortCode); + } + } else { + // Don't try to send any more messages to the parent if the request actor + // was killed. Set our state accordingly to Finished. + mReadyState = ReadyState::Finished; + mSentCommitOrAbort.Flip(); + IDB_LOG_MARK_CHILD_TRANSACTION( + "Request actor was killed, transaction will be aborted", + "IDBTransaction abort", LoggingSerialNumber()); + } + } +} + +void IDBTransaction::SendCommit(const bool aAutoCommit) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_SUCCEEDED(mAbortCode)); + MOZ_ASSERT(IsCommittingOrFinished()); + + // 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_CHILD_TRANSACTION_REQUEST( + "Committing transaction (%s)", "IDBTransaction commit (%s)", + LoggingSerialNumber(), requestSerialNumber, + aAutoCommit ? "automatically" : "explicitly"); + + const auto lastRequestSerialNumber = + [this, aAutoCommit, + requestSerialNumber]() -> Maybe<decltype(requestSerialNumber)> { + if (aAutoCommit) { + return Nothing(); + } + + // In case of an explicit commit, we need to note the serial number of the + // last request to check if a request submitted before the commit request + // failed. If we are currently in an event handler for a request on this + // transaction, ignore this request. This is used to synchronize the + // transaction's committing state with the parent side, to abort the + // transaction in case of a request resulting in an error (see + // https://w3c.github.io/IndexedDB/#async-execute-request, step 5.3.). With + // automatic commit, this is not necessary, as the transaction's state will + // only be set to committing after the last request completed. + const auto maybeCurrentTransaction = + BackgroundChildImpl::GetThreadLocalForCurrentThread() + ->mIndexedDBThreadLocal->MaybeCurrentTransactionRef(); + const bool dispatchingEventForThisTransaction = + maybeCurrentTransaction && &maybeCurrentTransaction.ref() == this; + + return Some(requestSerialNumber + ? (requestSerialNumber - + (dispatchingEventForThisTransaction ? 0 : 1)) + : 0); + }(); + + DoWithTransactionChild([lastRequestSerialNumber](auto& actor) { + actor.SendCommit(lastRequestSerialNumber); + }); + + mSentCommitOrAbort.Flip(); +} + +void IDBTransaction::SendAbort(const nsresult aResultCode) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aResultCode)); + MOZ_ASSERT(IsCommittingOrFinished()); + + // 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_CHILD_TRANSACTION_REQUEST( + "Aborting transaction with result 0x%" PRIx32, + "IDBTransaction abort (0x%" PRIx32 ")", LoggingSerialNumber(), + requestSerialNumber, static_cast<uint32_t>(aResultCode)); + + DoWithTransactionChild( + [aResultCode](auto& actor) { actor.SendAbort(aResultCode); }); + + mSentCommitOrAbort.Flip(); +} + +void IDBTransaction::NoteActiveTransaction() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mNotedActiveTransaction); + + mDatabase->NoteActiveTransaction(); + mNotedActiveTransaction = true; +} + +void IDBTransaction::MaybeNoteInactiveTransaction() { + AssertIsOnOwningThread(); + + if (mNotedActiveTransaction) { + mDatabase->NoteInactiveTransaction(); + mNotedActiveTransaction = false; + } +} + +void IDBTransaction::GetCallerLocation(nsAString& aFilename, + uint32_t* const aLineNo, + uint32_t* const aColumn) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLineNo); + MOZ_ASSERT(aColumn); + + aFilename = mFilename; + *aLineNo = mLineNo; + *aColumn = mColumn; +} + +RefPtr<IDBObjectStore> IDBTransaction::CreateObjectStore( + ObjectStoreSpec& aSpec) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aSpec.metadata().id()); + MOZ_ASSERT(Mode::VersionChange == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsActive()); + +#ifdef DEBUG + { + // TODO: Bind name outside of lambda capture as a workaround for GCC 7 bug + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66735. + const auto& name = aSpec.metadata().name(); + // TODO: Use #ifdef and local variable as a workaround for Bug 1583449. + const bool objectStoreNameDoesNotYetExist = + std::all_of(mObjectStores.cbegin(), mObjectStores.cend(), + [&name](const auto& objectStore) { + return objectStore->Name() != name; + }); + MOZ_ASSERT(objectStoreNameDoesNotYetExist); + } +#endif + + MOZ_ALWAYS_TRUE( + mBackgroundActor.mVersionChangeBackgroundActor->SendCreateObjectStore( + aSpec.metadata())); + + RefPtr<IDBObjectStore> objectStore = IDBObjectStore::Create( + SafeRefPtr{this, AcquireStrongRefFromRawPtr{}}, aSpec); + MOZ_ASSERT(objectStore); + + mObjectStores.AppendElement(objectStore); + + return objectStore; +} + +void IDBTransaction::DeleteObjectStore(const int64_t aObjectStoreId) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(Mode::VersionChange == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsActive()); + + MOZ_ALWAYS_TRUE( + mBackgroundActor.mVersionChangeBackgroundActor->SendDeleteObjectStore( + aObjectStoreId)); + + const auto foundIt = + std::find_if(mObjectStores.begin(), mObjectStores.end(), + [aObjectStoreId](const auto& objectStore) { + return objectStore->Id() == aObjectStoreId; + }); + if (foundIt != mObjectStores.end()) { + auto& objectStore = *foundIt; + objectStore->NoteDeletion(); + + RefPtr<IDBObjectStore>* deletedObjectStore = + mDeletedObjectStores.AppendElement(); + deletedObjectStore->swap(objectStore); + + mObjectStores.RemoveElementAt(foundIt); + } +} + +void IDBTransaction::RenameObjectStore(const int64_t aObjectStoreId, + const nsAString& aName) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStoreId); + MOZ_ASSERT(Mode::VersionChange == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsActive()); + + MOZ_ALWAYS_TRUE( + mBackgroundActor.mVersionChangeBackgroundActor->SendRenameObjectStore( + aObjectStoreId, nsString(aName))); +} + +void IDBTransaction::CreateIndex( + IDBObjectStore* const aObjectStore, + const indexedDB::IndexMetadata& aMetadata) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStore); + MOZ_ASSERT(aMetadata.id()); + MOZ_ASSERT(Mode::VersionChange == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsActive()); + + MOZ_ALWAYS_TRUE( + mBackgroundActor.mVersionChangeBackgroundActor->SendCreateIndex( + aObjectStore->Id(), aMetadata)); +} + +void IDBTransaction::DeleteIndex(IDBObjectStore* const aObjectStore, + const int64_t aIndexId) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStore); + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(Mode::VersionChange == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsActive()); + + MOZ_ALWAYS_TRUE( + mBackgroundActor.mVersionChangeBackgroundActor->SendDeleteIndex( + aObjectStore->Id(), aIndexId)); +} + +void IDBTransaction::RenameIndex(IDBObjectStore* const aObjectStore, + const int64_t aIndexId, + const nsAString& aName) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aObjectStore); + MOZ_ASSERT(aIndexId); + MOZ_ASSERT(Mode::VersionChange == mMode); + MOZ_ASSERT(mBackgroundActor.mVersionChangeBackgroundActor); + MOZ_ASSERT(IsActive()); + + MOZ_ALWAYS_TRUE( + mBackgroundActor.mVersionChangeBackgroundActor->SendRenameIndex( + aObjectStore->Id(), aIndexId, nsString(aName))); +} + +void IDBTransaction::AbortInternal(const nsresult aAbortCode, + RefPtr<DOMException> aError) { + AssertIsOnOwningThread(); + MOZ_ASSERT(NS_FAILED(aAbortCode)); + MOZ_ASSERT(!IsCommittingOrFinished()); + + const bool isVersionChange = mMode == Mode::VersionChange; + const bool needToSendAbort = !mStarted; + + mAbortCode = aAbortCode; + mReadyState = ReadyState::Finished; + mError = std::move(aError); + + 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 (!mDatabase->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->IsActive(). + + const nsTArray<ObjectStoreSpec>& specArray = + mDatabase->Spec()->objectStores(); + + if (specArray.IsEmpty()) { + // This case is specially handled as a performance optimization, it is + // equivalent to the else block. + mObjectStores.Clear(); + } else { + const auto validIds = TransformToHashtable<nsUint64HashKey>( + specArray, [](const auto& spec) { + const int64_t objectStoreId = spec.metadata().id(); + MOZ_ASSERT(objectStoreId); + return static_cast<uint64_t>(objectStoreId); + }); + + mObjectStores.RemoveLastElements( + mObjectStores.end() - + std::remove_if(mObjectStores.begin(), mObjectStores.end(), + [&validIds](const auto& objectStore) { + return !validIds.Contains( + uint64_t(objectStore->Id())); + })); + + std::copy_if(std::make_move_iterator(mDeletedObjectStores.begin()), + std::make_move_iterator(mDeletedObjectStores.end()), + MakeBackInserter(mObjectStores), + [&validIds](const auto& deletedObjectStore) { + const int64_t objectStoreId = deletedObjectStore->Id(); + MOZ_ASSERT(objectStoreId); + return validIds.Contains(uint64_t(objectStoreId)); + }); + } + 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* const aRequest) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aRequest); + + if (IsCommittingOrFinished()) { + // Already started (and maybe finished) the commit or abort so there is + // nothing to do here. + return; + } + + ErrorResult rv; + RefPtr<DOMException> error = aRequest->GetError(rv); + + // TODO: Do we deliberately ignore rv here? Isn't there a static analysis that + // prevents that? + + AbortInternal(aRequest->GetErrorCode(), std::move(error)); +} + +void IDBTransaction::Abort(const nsresult aErrorCode) { + AssertIsOnOwningThread(); + + if (IsCommittingOrFinished()) { + // Already started (and maybe finished) the commit or abort so there is + // nothing to do here. + return; + } + + AbortInternal(aErrorCode, DOMException::Create(aErrorCode)); +} + +// Specified by https://w3c.github.io/IndexedDB/#dom-idbtransaction-abort. +void IDBTransaction::Abort(ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (IsCommittingOrFinished()) { + aRv = NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR; + return; + } + + mReadyState = ReadyState::Inactive; + + AbortInternal(NS_ERROR_DOM_INDEXEDDB_ABORT_ERR, nullptr); + + mAbortedByScript.Flip(); +} + +// Specified by https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit. +void IDBTransaction::Commit(ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (mReadyState != ReadyState::Active || !mNotedActiveTransaction) { + aRv = NS_ERROR_DOM_INVALID_STATE_ERR; + return; + } + + MOZ_ASSERT(!mSentCommitOrAbort); + + MOZ_ASSERT(mReadyState == ReadyState::Active); + mReadyState = ReadyState::Committing; + if (NS_WARN_IF(NS_FAILED(mAbortCode))) { + SendAbort(mAbortCode); + aRv = mAbortCode; + return; + } + +#ifdef DEBUG + mWasExplicitlyCommitted.Flip(); +#endif + + SendCommit(false); +} + +void IDBTransaction::FireCompleteOrAbortEvents(const nsresult aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mFiredCompleteOrAbort); + + mReadyState = ReadyState::Finished; + +#ifdef DEBUG + mFiredCompleteOrAbort.Flip(); +#endif + + // Make sure we drop the WorkerRef when this function completes. + const auto scopeExit = MakeScopeExit([&] { mWorkerRef = nullptr; }); + + RefPtr<Event> event; + if (NS_SUCCEEDED(aResult)) { + event = CreateGenericEvent(this, nsDependentString(kCompleteEventType), + eDoesNotBubble, eNotCancelable); + MOZ_ASSERT(event); + + // If we hit this assertion, it probably means transaction object on the + // parent process doesn't propagate error properly. + MOZ_ASSERT(NS_SUCCEEDED(mAbortCode)); + } else { + if (aResult == NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR) { + mDatabase->SetQuotaExceeded(); + } + + if (!mError && !mAbortedByScript) { + mError = DOMException::Create(aResult); + } + + event = CreateGenericEvent(this, nsDependentString(kAbortEventType), + eDoesBubble, eNotCancelable); + MOZ_ASSERT(event); + + if (NS_SUCCEEDED(mAbortCode)) { + mAbortCode = aResult; + } + } + + if (NS_SUCCEEDED(mAbortCode)) { + IDB_LOG_MARK_CHILD_TRANSACTION("Firing 'complete' event", + "IDBTransaction 'complete' event", + mLoggingSerialNumber); + } else { + IDB_LOG_MARK_CHILD_TRANSACTION( + "Firing 'abort' event with error 0x%" PRIx32, + "IDBTransaction 'abort' event (0x%" PRIx32 ")", mLoggingSerialNumber, + static_cast<uint32_t>(mAbortCode)); + } + + IgnoredErrorResult rv; + DispatchEvent(*event, rv); + if (rv.Failed()) { + NS_WARNING("DispatchEvent failed!"); + } + + // Normally, we note inactive transaction here instead of + // IDBTransaction::ClearBackgroundActor() because here is the earliest place + // to know that it becomes non-blocking to allow the scheduler to start the + // preemption as soon as it can. + // Note: If the IDBTransaction object is held by the script, + // ClearBackgroundActor() will be done in ~IDBTransaction() until garbage + // collected after its window is closed which prevents us to preempt its + // window immediately after committed. + MaybeNoteInactiveTransaction(); +} + +int64_t IDBTransaction::NextObjectStoreId() { + AssertIsOnOwningThread(); + MOZ_ASSERT(Mode::VersionChange == mMode); + + return mNextObjectStoreId++; +} + +int64_t IDBTransaction::NextIndexId() { + AssertIsOnOwningThread(); + MOZ_ASSERT(Mode::VersionChange == mMode); + + return mNextIndexId++; +} + +void IDBTransaction::InvalidateCursorCaches() { + AssertIsOnOwningThread(); + + for (const auto& cursor : mCursors) { + cursor->InvalidateCachedResponses(); + } +} + +void IDBTransaction::RegisterCursor(IDBCursor& aCursor) { + AssertIsOnOwningThread(); + + mCursors.AppendElement(WrapNotNullUnchecked(&aCursor)); +} + +void IDBTransaction::UnregisterCursor(IDBCursor& aCursor) { + AssertIsOnOwningThread(); + + DebugOnly<bool> removed = mCursors.RemoveElement(&aCursor); + MOZ_ASSERT(removed); +} + +nsIGlobalObject* IDBTransaction::GetParentObject() const { + AssertIsOnOwningThread(); + + return mDatabase->GetParentObject(); +} + +IDBTransactionMode IDBTransaction::GetMode(ErrorResult& aRv) const { + AssertIsOnOwningThread(); + + switch (mMode) { + case Mode::ReadOnly: + return IDBTransactionMode::Readonly; + + case Mode::ReadWrite: + return IDBTransactionMode::Readwrite; + + case Mode::ReadWriteFlush: + return IDBTransactionMode::Readwriteflush; + + case Mode::Cleanup: + return IDBTransactionMode::Cleanup; + + case Mode::VersionChange: + return IDBTransactionMode::Versionchange; + + case Mode::Invalid: + default: + MOZ_CRASH("Bad mode!"); + } +} + +DOMException* IDBTransaction::GetError() const { + AssertIsOnOwningThread(); + + return mError; +} + +RefPtr<DOMStringList> IDBTransaction::ObjectStoreNames() const { + AssertIsOnOwningThread(); + + if (mMode == Mode::VersionChange) { + return mDatabase->ObjectStoreNames(); + } + + auto list = MakeRefPtr<DOMStringList>(); + list->StringArray() = mObjectStoreNames.Clone(); + return list; +} + +RefPtr<IDBObjectStore> IDBTransaction::ObjectStore(const nsAString& aName, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + if (IsCommittingOrFinished()) { + aRv.ThrowInvalidStateError("Transaction is already committing or done."); + return nullptr; + } + + auto* const spec = [this, &aName]() -> ObjectStoreSpec* { + if (IDBTransaction::Mode::VersionChange == mMode || + mObjectStoreNames.Contains(aName)) { + return mDatabase->LookupModifiableObjectStoreSpec( + [&aName](const auto& objectStore) { + return objectStore.metadata().name() == aName; + }); + } + return nullptr; + }(); + + if (!spec) { + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_FOUND_ERR); + return nullptr; + } + + RefPtr<IDBObjectStore> objectStore; + + const auto foundIt = std::find_if( + mObjectStores.cbegin(), mObjectStores.cend(), + [desiredId = spec->metadata().id()](const auto& existingObjectStore) { + return existingObjectStore->Id() == desiredId; + }); + if (foundIt != mObjectStores.cend()) { + objectStore = *foundIt; + } else { + objectStore = IDBObjectStore::Create( + SafeRefPtr{this, AcquireStrongRefFromRawPtr{}}, *spec); + MOZ_ASSERT(objectStore); + + mObjectStores.AppendElement(objectStore); + } + + return objectStore; +} + +NS_IMPL_ADDREF_INHERITED(IDBTransaction, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(IDBTransaction, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBTransaction) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(IDBTransaction) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(IDBTransaction, + DOMEventTargetHelper) + 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, + DOMEventTargetHelper) + // 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* const aCx, + JS::Handle<JSObject*> aGivenProto) { + AssertIsOnOwningThread(); + + return IDBTransaction_Binding::Wrap(aCx, this, std::move(aGivenProto)); +} + +void IDBTransaction::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + AssertIsOnOwningThread(); + + aVisitor.mCanHandle = true; + aVisitor.SetParentTarget(mDatabase, false); +} + +NS_IMETHODIMP +IDBTransaction::Run() { + AssertIsOnOwningThread(); + + // TODO: Instead of checking for Finished and Committing states here, we could + // remove the transaction from the pending IDB transactions list on + // abort/commit. + + if (ReadyState::Finished == mReadyState) { + // There are three cases where mReadyState is set to Finished: In + // FileCompleteOrAbortEvents, AbortInternal and in CommitIfNotStarted. We + // shouldn't get here after CommitIfNotStarted again. + MOZ_ASSERT(mFiredCompleteOrAbort || IsAborted()); + return NS_OK; + } + + if (ReadyState::Committing == mReadyState) { + MOZ_ASSERT(mSentCommitOrAbort); + return NS_OK; + } + // We're back at the event loop, no longer newborn, so + // return to Inactive state: + // https://w3c.github.io/IndexedDB/#cleanup-indexed-database-transactions. + MOZ_ASSERT(ReadyState::Active == mReadyState); + mReadyState = ReadyState::Inactive; + + CommitIfNotStarted(); + + return NS_OK; +} + +void IDBTransaction::CommitIfNotStarted() { + AssertIsOnOwningThread(); + + MOZ_ASSERT(ReadyState::Inactive == mReadyState); + + // Maybe commit if there were no requests generated. + if (!mStarted) { + MOZ_ASSERT(!mPendingRequestCount); + mReadyState = ReadyState::Finished; + + SendCommit(true); + } +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IDBTransaction.h b/dom/indexedDB/IDBTransaction.h new file mode 100644 index 0000000000..65fefddfe5 --- /dev/null +++ b/dom/indexedDB/IDBTransaction.h @@ -0,0 +1,366 @@ +/* -*- 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 "FlippedOnce.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDBTransactionBinding.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIRunnable.h" +#include "nsString.h" +#include "nsTArray.h" +#include "SafeRefPtr.h" + +namespace mozilla { + +class ErrorResult; +class EventChainPreVisitor; + +namespace dom { + +class DOMException; +class DOMStringList; +class IDBCursor; +class IDBDatabase; +class IDBObjectStore; +class IDBOpenDBRequest; +class IDBRequest; +class StrongWorkerRef; + +namespace indexedDB { +class PBackgroundIDBCursorChild; +class BackgroundRequestChild; +class BackgroundTransactionChild; +class BackgroundVersionChangeTransactionChild; +class IndexMetadata; +class ObjectStoreSpec; +class OpenCursorParams; +class RequestParams; +} // namespace indexedDB + +class IDBTransaction final + : public DOMEventTargetHelper, + public nsIRunnable, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + friend class indexedDB::BackgroundRequestChild; + + public: + enum struct Mode { + ReadOnly = 0, + ReadWrite, + ReadWriteFlush, + Cleanup, + VersionChange, + + // Only needed for IPC serialization helper, should never be used in code. + Invalid + }; + + enum struct ReadyState { Active, Inactive, Committing, Finished }; + + private: + // TODO: Only non-const because of Bug 1575173. + RefPtr<IDBDatabase> mDatabase; + RefPtr<DOMException> mError; + const nsTArray<nsString> mObjectStoreNames; + nsTArray<RefPtr<IDBObjectStore>> mObjectStores; + nsTArray<RefPtr<IDBObjectStore>> mDeletedObjectStores; + RefPtr<StrongWorkerRef> mWorkerRef; + nsTArray<NotNull<IDBCursor*>> mCursors; + + // Tagged with mMode. If mMode is Mode::VersionChange 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 Mode::VersionChange transactions. + int64_t mNextObjectStoreId; + int64_t mNextIndexId; + + nsresult mAbortCode; ///< The result that caused the transaction to be + ///< aborted, or NS_OK if not aborted. + ///< NS_ERROR_DOM_INDEXEDDB_ABORT_ERR indicates that the + ///< user explicitly requested aborting. Should be + ///< renamed to mResult or so, because it is actually + ///< used to check if the transaction has been aborted. + uint32_t mPendingRequestCount; ///< Counted via OnNewRequest and + ///< OnRequestFinished, so that the + ///< transaction can auto-commit when the last + ///< pending request finished. + + const nsString mFilename; + const uint32_t mLineNo; + const uint32_t mColumn; + + ReadyState mReadyState = ReadyState::Active; + FlippedOnce<false> mStarted; + const Mode mMode; + + bool mRegistered; ///< Whether mDatabase->RegisterTransaction() has been + ///< called (which may not be the case if construction was + ///< incomplete). + FlippedOnce<false> mAbortedByScript; + bool mNotedActiveTransaction; + FlippedOnce<false> mSentCommitOrAbort; + +#ifdef DEBUG + FlippedOnce<false> mFiredCompleteOrAbort; + FlippedOnce<false> mWasExplicitlyCommitted; +#endif + + public: + [[nodiscard]] static SafeRefPtr<IDBTransaction> CreateVersionChange( + IDBDatabase* aDatabase, + indexedDB::BackgroundVersionChangeTransactionChild* aActor, + NotNull<IDBOpenDBRequest*> aOpenRequest, int64_t aNextObjectStoreId, + int64_t aNextIndexId); + + [[nodiscard]] static SafeRefPtr<IDBTransaction> Create( + JSContext* aCx, IDBDatabase* aDatabase, + const nsTArray<nsString>& aObjectStoreNames, Mode aMode); + + static Maybe<IDBTransaction&> MaybeCurrent(); + + void AssertIsOnOwningThread() const +#ifdef DEBUG + ; +#else + { + } +#endif + + void SetBackgroundActor( + indexedDB::BackgroundTransactionChild* aBackgroundActor); + + void ClearBackgroundActor() { + AssertIsOnOwningThread(); + + if (mMode == Mode::VersionChange) { + mBackgroundActor.mVersionChangeBackgroundActor = nullptr; + } else { + mBackgroundActor.mNormalBackgroundActor = nullptr; + } + + // Note inactive transaction here if we didn't receive the Complete message + // from the parent. + MaybeNoteInactiveTransaction(); + } + + indexedDB::BackgroundRequestChild* StartRequest( + MovingNotNull<RefPtr<mozilla::dom::IDBRequest>> aRequest, + const indexedDB::RequestParams& aParams); + + void OpenCursor(indexedDB::PBackgroundIDBCursorChild& aBackgroundActor, + const indexedDB::OpenCursorParams& aParams); + + void RefreshSpec(bool aMayDelete); + + bool IsCommittingOrFinished() const { + AssertIsOnOwningThread(); + + return mReadyState == ReadyState::Committing || + mReadyState == ReadyState::Finished; + } + + bool IsActive() const { + AssertIsOnOwningThread(); + + return mReadyState == ReadyState::Active; + } + + bool IsInactive() const { + AssertIsOnOwningThread(); + + return mReadyState == ReadyState::Inactive; + } + + bool IsFinished() const { + AssertIsOnOwningThread(); + + return mReadyState == ReadyState::Finished; + } + + bool IsWriteAllowed() const { + AssertIsOnOwningThread(); + return mMode == Mode::ReadWrite || mMode == Mode::ReadWriteFlush || + mMode == Mode::Cleanup || mMode == Mode::VersionChange; + } + + bool IsAborted() const { + AssertIsOnOwningThread(); + return NS_FAILED(mAbortCode); + } + +#ifdef DEBUG + bool WasExplicitlyCommitted() const { return mWasExplicitlyCommitted; } +#endif + + void TransitionToActive() { + MOZ_ASSERT(mReadyState == ReadyState::Inactive); + mReadyState = ReadyState::Active; + } + + void TransitionToInactive() { + MOZ_ASSERT(mReadyState == ReadyState::Active); + mReadyState = ReadyState::Inactive; + } + + 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; + } + + uint32_t GetPendingRequestCount() const { return mPendingRequestCount; } + + IDBDatabase* Database() const { + AssertIsOnOwningThread(); + return mDatabase; + } + + // Only for use by ProfilerHelpers.h + const nsTArray<nsString>& ObjectStoreNamesInternal() const { + AssertIsOnOwningThread(); + return mObjectStoreNames; + } + + [[nodiscard]] RefPtr<IDBObjectStore> CreateObjectStore( + indexedDB::ObjectStoreSpec& aSpec); + + void DeleteObjectStore(int64_t aObjectStoreId); + + void RenameObjectStore(int64_t aObjectStoreId, const nsAString& aName) const; + + void CreateIndex(IDBObjectStore* aObjectStore, + const indexedDB::IndexMetadata& aMetadata) const; + + void DeleteIndex(IDBObjectStore* aObjectStore, int64_t aIndexId) const; + + void RenameIndex(IDBObjectStore* aObjectStore, int64_t aIndexId, + const nsAString& aName) const; + + void Abort(IDBRequest* aRequest); + + void Abort(nsresult aErrorCode); + + int64_t LoggingSerialNumber() const { + AssertIsOnOwningThread(); + + return mLoggingSerialNumber; + } + + nsIGlobalObject* GetParentObject() const; + + void FireCompleteOrAbortEvents(nsresult aResult); + + // Only for Mode::VersionChange transactions. + int64_t NextObjectStoreId(); + + // Only for Mode::VersionChange transactions. + int64_t NextIndexId(); + + void InvalidateCursorCaches(); + void RegisterCursor(IDBCursor& aCursor); + void UnregisterCursor(IDBCursor& aCursor); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(IDBTransaction, DOMEventTargetHelper) + + void CommitIfNotStarted(); + + // nsWrapperCache + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Methods bound via WebIDL. + IDBDatabase* Db() const { return Database(); } + + IDBTransactionMode GetMode(ErrorResult& aRv) const; + + DOMException* GetError() const; + + [[nodiscard]] RefPtr<IDBObjectStore> ObjectStore(const nsAString& aName, + ErrorResult& aRv); + + void Commit(ErrorResult& aRv); + + void Abort(ErrorResult& aRv); + + IMPL_EVENT_HANDLER(abort) + IMPL_EVENT_HANDLER(complete) + IMPL_EVENT_HANDLER(error) + + [[nodiscard]] RefPtr<DOMStringList> ObjectStoreNames() const; + + // EventTarget + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + private: + struct CreatedFromFactoryFunction {}; + + public: + IDBTransaction(IDBDatabase* aDatabase, + const nsTArray<nsString>& aObjectStoreNames, Mode aMode, + nsString aFilename, uint32_t aLineNo, uint32_t aColumn, + CreatedFromFactoryFunction aDummy); + + private: + ~IDBTransaction(); + + void AbortInternal(nsresult aAbortCode, RefPtr<DOMException> aError); + + void SendCommit(bool aAutoCommit); + + void SendAbort(nsresult aResultCode); + + void NoteActiveTransaction(); + + void MaybeNoteInactiveTransaction(); + + // TODO consider making private again, or move to the right place + public: + void OnNewRequest(); + + void OnRequestFinished(bool aRequestCompletedSuccessfully); + + private: + template <typename Func> + auto DoWithTransactionChild(const Func& aFunc) const; + + bool HasTransactionChild() const; +}; + +inline bool ReferenceEquals(const Maybe<IDBTransaction&>& aLHS, + const Maybe<IDBTransaction&>& aRHS) { + if (aLHS.isNothing() != aRHS.isNothing()) { + return false; + } + return aLHS.isNothing() || &aLHS.ref() == &aRHS.ref(); +} + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_idbtransaction_h__ diff --git a/dom/indexedDB/IndexedDBCipherKeyManager.cpp b/dom/indexedDB/IndexedDBCipherKeyManager.cpp new file mode 100644 index 0000000000..9e86809677 --- /dev/null +++ b/dom/indexedDB/IndexedDBCipherKeyManager.cpp @@ -0,0 +1,35 @@ +/* -*- 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 "IndexedDBCipherKeyManager.h" + +#include "mozilla/dom/quota/QuotaCommon.h" + +namespace mozilla::dom::indexedDB { + +Maybe<CipherKey> IndexedDBCipherKeyManager::Get(const nsACString& aKeyId) { + auto lockedCipherKeys = mCipherKeys.Lock(); + + return lockedCipherKeys->MaybeGet(aKeyId); +} + +CipherKey IndexedDBCipherKeyManager::Ensure(const nsACString& aKeyId) { + auto lockedCipherKeys = mCipherKeys.Lock(); + + return lockedCipherKeys->LookupOrInsertWith(aKeyId, [] { + // Generate a new key if one corresponding to keyStoreId does not exist + // already. + + QM_TRY_RETURN(IndexedDBCipherStrategy::GenerateKey(), [](const auto&) { + // Bug1800110 Propagate the error to the caller rather than asserting. + MOZ_RELEASE_ASSERT(false); + + return CipherKey{}; + }); + }); +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/IndexedDBCipherKeyManager.h b/dom/indexedDB/IndexedDBCipherKeyManager.h new file mode 100644 index 0000000000..aca02add97 --- /dev/null +++ b/dom/indexedDB/IndexedDBCipherKeyManager.h @@ -0,0 +1,44 @@ +/* -*- 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 DOM_INDEXEDDB_INDEXEDDBCIPHERKEYMANAGER_H_ +#define DOM_INDEXEDDB_INDEXEDDBCIPHERKEYMANAGER_H_ + +#include "mozilla/DataMutex.h" +#include "mozilla/dom/quota/IPCStreamCipherStrategy.h" +#include "nsTHashMap.h" + +namespace mozilla::dom::indexedDB { + +using IndexedDBCipherStrategy = quota::IPCStreamCipherStrategy; +using CipherKey = IndexedDBCipherStrategy::KeyType; + +class IndexedDBCipherKeyManager { + // This helper class is used by IndexedDB operations to store/retrieve cipher + // keys in private browsing mode. All data in IndexedDB must be encrypted + // using a cipher key and unique IV (Initialization Vector). While there's a + // separate cipher key for every blob file; the SQLite database gets encrypted + // using the commmon database key. All keys pertaining to a single IndexedDB + // database get stored together in a hashmap. So the hashmap can be used to + // to look up the common database key and blob keys using "default" and blob + // file ids respectively. + + public: + IndexedDBCipherKeyManager() : mCipherKeys("IndexedDBCipherKeyManager"){}; + + Maybe<CipherKey> Get(const nsACString& aKeyId = "default"_ns); + + CipherKey Ensure(const nsACString& aKeyId = "default"_ns); + + private: + // XXX Maybe we can avoid a mutex here by moving all accesses to the + // background thread. + DataMutex<nsTHashMap<nsCStringHashKey, CipherKey>> mCipherKeys; +}; + +} // namespace mozilla::dom::indexedDB + +#endif // DOM_INDEXEDDB_INDEXEDDBCIPHERKEYMANAGER_H_ diff --git a/dom/indexedDB/IndexedDBCommon.cpp b/dom/indexedDB/IndexedDBCommon.cpp new file mode 100644 index 0000000000..63a6fd2fb6 --- /dev/null +++ b/dom/indexedDB/IndexedDBCommon.cpp @@ -0,0 +1,40 @@ +/* -*- 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 "IndexedDBCommon.h" + +#include "js/StructuredClone.h" +#include "mozilla/SnappyUncompressInputStream.h" + +namespace mozilla::dom::indexedDB { + +// aStructuredCloneData is a parameter rather than a return value because one +// caller preallocates it on the heap not immediately before calling for some +// reason. Maybe this could be changed. +nsresult SnappyUncompressStructuredCloneData( + nsIInputStream& aInputStream, JSStructuredCloneData& aStructuredCloneData) { + const auto snappyInputStream = + MakeRefPtr<SnappyUncompressInputStream>(&aInputStream); + + char buffer[kFileCopyBufferSize]; + + QM_TRY(CollectEach( + [&snappyInputStream = *snappyInputStream, &buffer] { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(snappyInputStream, Read, + buffer, sizeof(buffer))); + }, + [&aStructuredCloneData, + &buffer](const uint32_t& numRead) -> Result<Ok, nsresult> { + QM_TRY(OkIf(aStructuredCloneData.AppendBytes(buffer, numRead)), + Err(NS_ERROR_OUT_OF_MEMORY)); + + return Ok{}; + })); + + return NS_OK; +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/IndexedDBCommon.h b/dom/indexedDB/IndexedDBCommon.h new file mode 100644 index 0000000000..b9b4996ab1 --- /dev/null +++ b/dom/indexedDB/IndexedDBCommon.h @@ -0,0 +1,24 @@ +/* -*- 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_IndexedDBCommon_h +#define mozilla_dom_indexeddb_IndexedDBCommon_h + +#include "mozilla/dom/quota/QuotaCommon.h" + +class JSStructuredCloneData; +class nsIInputStream; + +namespace mozilla::dom::indexedDB { + +static constexpr uint32_t kFileCopyBufferSize = 32768; + +nsresult SnappyUncompressStructuredCloneData( + nsIInputStream& aInputStream, JSStructuredCloneData& aStructuredCloneData); + +} // namespace mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_IndexedDBCommon_h diff --git a/dom/indexedDB/IndexedDatabase.cpp b/dom/indexedDB/IndexedDatabase.cpp new file mode 100644 index 0000000000..e480fb00ee --- /dev/null +++ b/dom/indexedDB/IndexedDatabase.cpp @@ -0,0 +1,448 @@ +/* -*- 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 "mozilla/dom/IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" + +#include "IDBDatabase.h" + +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/StructuredCloneTags.h" +#include "mozilla/dom/WorkerScope.h" +#include "MainThreadUtils.h" +#include "jsapi.h" +#include "nsIFile.h" +#include "nsIGlobalObject.h" +#include "nsQueryObject.h" +#include "nsString.h" + +namespace mozilla::dom::indexedDB { +namespace { +struct MOZ_STACK_CLASS MutableFileData final { + nsString type; + nsString name; + + MOZ_COUNTED_DEFAULT_CTOR(MutableFileData) + + MOZ_COUNTED_DTOR(MutableFileData) +}; + +struct MOZ_STACK_CLASS BlobOrFileData final { + uint32_t tag = 0; + uint64_t size = 0; + nsString type; + nsString name; + int64_t lastModifiedDate = INT64_MAX; + + MOZ_COUNTED_DEFAULT_CTOR(BlobOrFileData) + + MOZ_COUNTED_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); + } + + MOZ_COUNTED_DTOR(WasmModuleData) +}; + +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* const 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_MODULE == 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; +} + +template <typename StructuredCloneFile> +class ValueDeserializationHelper; + +class ValueDeserializationHelperBase { + public: + static bool CreateAndWrapWasmModule(JSContext* aCx, + const StructuredCloneFileBase& aFile, + const WasmModuleData& aData, + JS::MutableHandle<JSObject*> aResult) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eWasmBytecode); + + // Both on the parent and child side, just create a plain object here, + // support for de-serialization of WebAssembly.Modules has been removed in + // bug 1561876. Full removal is tracked in bug 1487479. + + JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); + if (NS_WARN_IF(!obj)) { + return false; + } + + aResult.set(obj); + return true; + } + + template <typename StructuredCloneFile> + static bool CreateAndWrapBlobOrFile(JSContext* aCx, IDBDatabase* aDatabase, + const 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.Type() == StructuredCloneFileBase::eBlob); + + const auto blob = ValueDeserializationHelper<StructuredCloneFile>::GetBlob( + aCx, aDatabase, aFile); + if (NS_WARN_IF(!blob)) { + return false; + } + + if (aData.tag == SCTAG_DOM_BLOB) { + blob->Impl()->SetLazyData(VoidString(), aData.type, aData.size, + INT64_MAX); + MOZ_ASSERT(!blob->IsFile()); + + // XXX The comment below is somewhat confusing, since it seems to imply + // that this branch is only executed when called from ActorsParent, but + // it's executed from both the parent and the child side code. + + // ActorsParent sends here a kind of half blob and half file wrapped into + // a DOM File object. DOM File and DOM Blob are a WebIDL wrapper around a + // BlobImpl object. SetLazyData() has just changed the BlobImpl to be a + // Blob (see the previous assert), but 'blob' still has the WebIDL DOM + // File wrapping. + // Before exposing it to content, we must recreate a DOM Blob object. + + const RefPtr<Blob> exposedBlob = + Blob::Create(blob->GetParentObject(), blob->Impl()); + if (NS_WARN_IF(!exposedBlob)) { + return false; + } + + return WrapAsJSObject(aCx, exposedBlob, aResult); + } + + blob->Impl()->SetLazyData(aData.name, aData.type, aData.size, + aData.lastModifiedDate * PR_USEC_PER_MSEC); + + MOZ_ASSERT(blob->IsFile()); + const RefPtr<File> file = blob->ToFile(); + MOZ_ASSERT(file); + + return WrapAsJSObject(aCx, file, aResult); + } +}; + +template <> +class ValueDeserializationHelper<StructuredCloneFileParent> + : public ValueDeserializationHelperBase { + public: + static bool CreateAndWrapMutableFile(JSContext* aCx, + StructuredCloneFileParent& aFile, + const MutableFileData& aData, + JS::MutableHandle<JSObject*> aResult) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eBlob); + + // We are in an IDB SQLite schema upgrade where we don't care about a real + // 'MutableFile', but we just care of having a proper |mType| flag. + + aFile.MutateType(StructuredCloneFileBase::eMutableFile); + + // 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 RefPtr<Blob> GetBlob(JSContext* aCx, IDBDatabase* aDatabase, + const StructuredCloneFileParent& aFile) { + // This is chrome code, so there is no parent, but still we want to set a + // correct parent for the new File object. + const auto global = [aDatabase, aCx]() -> nsCOMPtr<nsIGlobalObject> { + if (NS_IsMainThread()) { + if (aDatabase && aDatabase->GetParentObject()) { + return aDatabase->GetParentObject(); + } + return xpc::CurrentNativeGlobal(aCx); + } + const WorkerPrivate* const workerPrivate = + GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + WorkerGlobalScope* const globalScope = workerPrivate->GlobalScope(); + MOZ_ASSERT(globalScope); + + return do_QueryObject(globalScope); + }(); + + MOZ_ASSERT(global); + + // We do not have an mBlob but do have a DatabaseFileInfo. + // + // If we are creating an index, we do need a real-looking Blob/File instance + // because the index's key path can reference their properties. Rather than + // create a fake-looking object, create a real Blob. + // + // If we are in a schema upgrade, we don't strictly need that, but we do not + // need to optimize for that, and create it anyway. + const nsCOMPtr<nsIFile> file = aFile.FileInfo().GetFileForFileInfo(); + if (!file) { + return nullptr; + } + + const auto impl = MakeRefPtr<FileBlobImpl>(file); + impl->SetFileId(aFile.FileInfo().Id()); + return File::Create(global, impl); + } +}; + +template <> +class ValueDeserializationHelper<StructuredCloneFileChild> + : public ValueDeserializationHelperBase { + public: + static bool CreateAndWrapMutableFile(JSContext* aCx, + StructuredCloneFileChild& aFile, + const MutableFileData& aData, + JS::MutableHandle<JSObject*> aResult) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aFile.Type() == StructuredCloneFileBase::eMutableFile); + + return false; + } + + static RefPtr<Blob> GetBlob(JSContext* aCx, IDBDatabase* aDatabase, + const StructuredCloneFileChild& aFile) { + if (aFile.HasBlob()) { + return aFile.BlobPtr(); + } + + MOZ_CRASH("Expected a StructuredCloneFile with a Blob"); + } +}; + +} // namespace + +template <typename StructuredCloneReadInfo> +JSObject* CommonStructuredCloneReadCallback( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, + StructuredCloneReadInfo* aCloneReadInfo, IDBDatabase* aDatabase) { + // 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_MODULE == 0xffff8006, + "You changed our structured clone tag values and just ate " + "everyone's IndexedDB data. I hope you are happy."); + + using StructuredCloneFile = + typename StructuredCloneReadInfo::StructuredCloneFile; + + if (aTag == SCTAG_DOM_FILE_WITHOUT_LASTMODIFIEDDATE || + aTag == SCTAG_DOM_BLOB || aTag == SCTAG_DOM_FILE || + aTag == SCTAG_DOM_MUTABLEFILE || aTag == SCTAG_DOM_WASM_MODULE) { + JS::Rooted<JSObject*> result(aCx); + + if (aTag == SCTAG_DOM_WASM_MODULE) { + WasmModuleData data(aData); + if (NS_WARN_IF(!ReadWasmModule(aReader, &data))) { + return nullptr; + } + + MOZ_ASSERT(data.compiledIndex == data.bytecodeIndex + 1); + MOZ_ASSERT(!data.flags); + + const auto& files = aCloneReadInfo->Files(); + if (data.bytecodeIndex >= files.Length() || + data.compiledIndex >= files.Length()) { + MOZ_ASSERT(false, "Bad index value!"); + return nullptr; + } + + const auto& file = files[data.bytecodeIndex]; + + if (NS_WARN_IF(!ValueDeserializationHelper<StructuredCloneFile>:: + CreateAndWrapWasmModule(aCx, file, data, &result))) { + return nullptr; + } + + return result; + } + + if (aData >= aCloneReadInfo->Files().Length()) { + MOZ_ASSERT(false, "Bad index value!"); + return nullptr; + } + + auto& file = aCloneReadInfo->MutableFile(aData); + + if (aTag == SCTAG_DOM_MUTABLEFILE) { + MutableFileData data; + if (NS_WARN_IF(!ReadFileHandle(aReader, &data))) { + return nullptr; + } + + if (NS_WARN_IF(!ValueDeserializationHelper<StructuredCloneFile>:: + 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(!ValueDeserializationHelper< + StructuredCloneFile>::CreateAndWrapBlobOrFile(aCx, aDatabase, + file, data, + &result))) { + return nullptr; + } + + return result; + } + + return StructuredCloneHolder::ReadFullySerializableObjects(aCx, aReader, + aTag); +} + +template JSObject* CommonStructuredCloneReadCallback( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, + StructuredCloneReadInfoChild* aCloneReadInfo, IDBDatabase* aDatabase); + +template JSObject* CommonStructuredCloneReadCallback( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, + StructuredCloneReadInfoParent* aCloneReadInfo, IDBDatabase* aDatabase); +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/IndexedDatabase.h b/dom/indexedDB/IndexedDatabase.h new file mode 100644 index 0000000000..6fdf641553 --- /dev/null +++ b/dom/indexedDB/IndexedDatabase.h @@ -0,0 +1,232 @@ +/* -*- 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 "DatabaseFileInfoFwd.h" +#include "js/StructuredClone.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/Variant.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "SafeRefPtr.h" + +namespace mozilla::dom { + +class Blob; +class IDBDatabase; + +namespace indexedDB { + +struct StructuredCloneFileBase { + enum FileType { + eBlob, + eMutableFile, + eStructuredClone, + eWasmBytecode, + eWasmCompiled, + eEndGuard + }; + + FileType Type() const { return mType; } + + protected: + explicit StructuredCloneFileBase(FileType aType) : mType{aType} {} + + FileType mType; +}; + +struct StructuredCloneFileChild : StructuredCloneFileBase { + StructuredCloneFileChild(const StructuredCloneFileChild&) = delete; + StructuredCloneFileChild& operator=(const StructuredCloneFileChild&) = delete; +#ifdef NS_BUILD_REFCNT_LOGGING + // In IndexedDatabaseInlines.h + StructuredCloneFileChild(StructuredCloneFileChild&&); +#else + StructuredCloneFileChild(StructuredCloneFileChild&&) = default; +#endif + StructuredCloneFileChild& operator=(StructuredCloneFileChild&&) = delete; + + // In IndexedDatabaseInlines.h + ~StructuredCloneFileChild(); + + // In IndexedDatabaseInlines.h + explicit StructuredCloneFileChild(FileType aType); + + // In IndexedDatabaseInlines.h + StructuredCloneFileChild(FileType aType, RefPtr<Blob> aBlob); + + const dom::Blob& Blob() const { return *mContents->as<RefPtr<dom::Blob>>(); } + + // XXX This is currently used for a number of reasons. Bug 1620560 will remove + // the need for one of them, but the uses of do_GetWeakReference in + // IDBDatabase::GetOrCreateFileActorForBlob and WrapAsJSObject in + // CopyingStructuredCloneReadCallback are probably harder to change. + dom::Blob& MutableBlob() const { return *mContents->as<RefPtr<dom::Blob>>(); } + + // In IndexedDatabaseInlines.h + RefPtr<dom::Blob> BlobPtr() const; + + bool HasBlob() const { return mContents->is<RefPtr<dom::Blob>>(); } + + private: + InitializedOnce<const Variant<Nothing, RefPtr<dom::Blob>>> mContents; +}; + +struct StructuredCloneFileParent : StructuredCloneFileBase { + StructuredCloneFileParent(const StructuredCloneFileParent&) = delete; + StructuredCloneFileParent& operator=(const StructuredCloneFileParent&) = + delete; +#ifdef NS_BUILD_REFCNT_LOGGING + // In IndexedDatabaseInlines.h + StructuredCloneFileParent(StructuredCloneFileParent&&); +#else + StructuredCloneFileParent(StructuredCloneFileParent&&) = default; +#endif + StructuredCloneFileParent& operator=(StructuredCloneFileParent&&) = delete; + + // In IndexedDatabaseInlines.h + StructuredCloneFileParent(FileType aType, + SafeRefPtr<DatabaseFileInfo> aFileInfo); + + // In IndexedDatabaseInlines.h + ~StructuredCloneFileParent(); + + // XXX This is used for a schema upgrade hack in UpgradeSchemaFrom19_0To20_0. + // When this is eventually removed, this function can be removed, and mType + // can be declared const in the base class. + void MutateType(FileType aNewType) { mType = aNewType; } + + const DatabaseFileInfo& FileInfo() const { return ***mContents; } + + // In IndexedDatabaseInlines.h + SafeRefPtr<DatabaseFileInfo> FileInfoPtr() const; + + private: + InitializedOnce<const Maybe<SafeRefPtr<DatabaseFileInfo>>> mContents; +}; + +struct StructuredCloneReadInfoBase { + // In IndexedDatabaseInlines.h + explicit StructuredCloneReadInfoBase(JSStructuredCloneData&& aData) + : mData{std::move(aData)} {} + + const JSStructuredCloneData& Data() const { return mData; } + JSStructuredCloneData ReleaseData() { return std::move(mData); } + + private: + JSStructuredCloneData mData; +}; + +template <typename StructuredCloneFileT> +struct StructuredCloneReadInfo : StructuredCloneReadInfoBase { + using StructuredCloneFile = StructuredCloneFileT; + + // In IndexedDatabaseInlines.h + explicit StructuredCloneReadInfo(JS::StructuredCloneScope aScope); + + // In IndexedDatabaseInlines.h + StructuredCloneReadInfo(); + + // In IndexedDatabaseInlines.h + StructuredCloneReadInfo(JSStructuredCloneData&& aData, + nsTArray<StructuredCloneFile> aFiles); + +#ifdef NS_BUILD_REFCNT_LOGGING + // In IndexedDatabaseInlines.h + ~StructuredCloneReadInfo(); + + // In IndexedDatabaseInlines.h + // + // This custom implementation of the move ctor is only necessary because of + // MOZ_COUNT_CTOR. It is less efficient as the compiler-generated move ctor, + // since it unnecessarily clears elements on the source. + StructuredCloneReadInfo(StructuredCloneReadInfo&& aOther) noexcept; +#else + StructuredCloneReadInfo(StructuredCloneReadInfo&& aOther) = default; +#endif + StructuredCloneReadInfo& operator=(StructuredCloneReadInfo&& aOther) = + default; + + StructuredCloneReadInfo(const StructuredCloneReadInfo& aOther) = delete; + StructuredCloneReadInfo& operator=(const StructuredCloneReadInfo& aOther) = + delete; + + // In IndexedDatabaseInlines.h + size_t Size() const; + + // XXX This is only needed for a schema upgrade (UpgradeSchemaFrom19_0To20_0). + // If support for older schemas is dropped, we can probably remove this method + // and make mFiles InitializedOnce. + StructuredCloneFile& MutableFile(const size_t aIndex) { + return mFiles[aIndex]; + } + const nsTArray<StructuredCloneFile>& Files() const { return mFiles; } + + nsTArray<StructuredCloneFile> ReleaseFiles() { return std::move(mFiles); } + + bool HasFiles() const { return !mFiles.IsEmpty(); } + + private: + nsTArray<StructuredCloneFile> mFiles; +}; + +struct StructuredCloneReadInfoChild + : StructuredCloneReadInfo<StructuredCloneFileChild> { + inline StructuredCloneReadInfoChild(JSStructuredCloneData&& aData, + nsTArray<StructuredCloneFileChild> aFiles, + IDBDatabase* aDatabase); + + IDBDatabase* Database() const { return mDatabase; } + + private: + IDBDatabase* mDatabase; +}; + +// This is only defined in the header file to satisfy the clang-plugin static +// analysis, it could be placed in ActorsParent.cpp otherwise. +struct StructuredCloneReadInfoParent + : StructuredCloneReadInfo<StructuredCloneFileParent> { + StructuredCloneReadInfoParent(JSStructuredCloneData&& aData, + nsTArray<StructuredCloneFileParent> aFiles, + bool aHasPreprocessInfo) + : StructuredCloneReadInfo{std::move(aData), std::move(aFiles)}, + mHasPreprocessInfo{aHasPreprocessInfo} {} + + bool HasPreprocessInfo() const { return mHasPreprocessInfo; } + + private: + bool mHasPreprocessInfo; +}; + +template <typename StructuredCloneReadInfo> +JSObject* CommonStructuredCloneReadCallback( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, + StructuredCloneReadInfo* aCloneReadInfo, IDBDatabase* aDatabase); + +template <typename StructuredCloneReadInfoType> +JSObject* StructuredCloneReadCallback( + JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aData, + void* aClosure); + +} // namespace indexedDB +} // namespace mozilla::dom + +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR( + mozilla::dom::indexedDB::StructuredCloneReadInfo< + mozilla::dom::indexedDB::StructuredCloneFileChild>); +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR( + mozilla::dom::indexedDB::StructuredCloneReadInfo< + mozilla::dom::indexedDB::StructuredCloneFileParent>); +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR( + mozilla::dom::indexedDB::StructuredCloneReadInfoChild); +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR( + mozilla::dom::indexedDB::StructuredCloneReadInfoParent); + +#endif // mozilla_dom_indexeddatabase_h__ diff --git a/dom/indexedDB/IndexedDatabaseInlines.h b/dom/indexedDB/IndexedDatabaseInlines.h new file mode 100644 index 0000000000..92782f3202 --- /dev/null +++ b/dom/indexedDB/IndexedDatabaseInlines.h @@ -0,0 +1,182 @@ +/* -*- 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 "DatabaseFileInfo.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/File.h" + +namespace mozilla::dom::indexedDB { + +#ifdef NS_BUILD_REFCNT_LOGGING +inline StructuredCloneFileChild::StructuredCloneFileChild( + StructuredCloneFileChild&& aOther) + : StructuredCloneFileBase{std::move(aOther)}, + mContents{std::move(aOther.mContents)} { + MOZ_COUNT_CTOR(StructuredCloneFileChild); +} +#endif + +inline StructuredCloneFileChild::~StructuredCloneFileChild() { + MOZ_COUNT_DTOR(StructuredCloneFileChild); +} + +inline StructuredCloneFileChild::StructuredCloneFileChild(FileType aType) + : StructuredCloneFileBase{aType}, mContents{Nothing()} { + MOZ_COUNT_CTOR(StructuredCloneFileChild); +} + +inline StructuredCloneFileChild::StructuredCloneFileChild( + FileType aType, RefPtr<dom::Blob> aBlob) + : StructuredCloneFileBase{aType}, mContents{std::move(aBlob)} { + MOZ_ASSERT(eBlob == aType || eStructuredClone == aType); + MOZ_ASSERT(mContents->as<RefPtr<dom::Blob>>()); + MOZ_COUNT_CTOR(StructuredCloneFileChild); +} + +inline StructuredCloneFileParent::StructuredCloneFileParent( + FileType aType, SafeRefPtr<DatabaseFileInfo> aFileInfo) + : StructuredCloneFileBase{aType}, mContents{Some(std::move(aFileInfo))} { + MOZ_ASSERT(**mContents); + MOZ_COUNT_CTOR(StructuredCloneFileParent); +} + +#ifdef NS_BUILD_REFCNT_LOGGING +inline StructuredCloneFileParent::StructuredCloneFileParent( + StructuredCloneFileParent&& aOther) + : StructuredCloneFileBase{std::move(aOther)}, + mContents{std::move(aOther.mContents)} { + MOZ_COUNT_CTOR(StructuredCloneFileParent); +} +#endif + +inline StructuredCloneFileParent::~StructuredCloneFileParent() { + MOZ_COUNT_DTOR(StructuredCloneFileParent); +} + +inline SafeRefPtr<DatabaseFileInfo> StructuredCloneFileParent::FileInfoPtr() + const { + return (*mContents)->clonePtr(); +} + +inline RefPtr<dom::Blob> StructuredCloneFileChild::BlobPtr() const { + return mContents->as<RefPtr<dom::Blob>>(); +} + +template <typename StructuredCloneFile> +inline StructuredCloneReadInfo<StructuredCloneFile>::StructuredCloneReadInfo( + JS::StructuredCloneScope aScope) + : StructuredCloneReadInfoBase(JSStructuredCloneData{aScope}) { + MOZ_COUNT_CTOR(StructuredCloneReadInfo); +} + +template <typename StructuredCloneFile> +inline StructuredCloneReadInfo<StructuredCloneFile>::StructuredCloneReadInfo() + : StructuredCloneReadInfo( + JS::StructuredCloneScope::DifferentProcessForIndexedDB) {} + +template <typename StructuredCloneFile> +inline StructuredCloneReadInfo<StructuredCloneFile>::StructuredCloneReadInfo( + JSStructuredCloneData&& aData, nsTArray<StructuredCloneFile> aFiles) + : StructuredCloneReadInfoBase{std::move(aData)}, mFiles{std::move(aFiles)} { + MOZ_COUNT_CTOR(StructuredCloneReadInfo); +} + +#ifdef NS_BUILD_REFCNT_LOGGING +template <typename StructuredCloneFile> +inline StructuredCloneReadInfo<StructuredCloneFile>::StructuredCloneReadInfo( + StructuredCloneReadInfo&& aOther) noexcept + : StructuredCloneReadInfoBase{std::move(aOther)}, + mFiles{std::move(aOther.mFiles)} { + MOZ_COUNT_CTOR(StructuredCloneReadInfo); +} + +template <typename StructuredCloneFile> +inline StructuredCloneReadInfo< + StructuredCloneFile>::~StructuredCloneReadInfo() { + MOZ_COUNT_DTOR(StructuredCloneReadInfo); +} + +#endif + +template <typename StructuredCloneFile> +inline size_t StructuredCloneReadInfo<StructuredCloneFile>::Size() const { + size_t size = Data().Size(); + + for (uint32_t i = 0, count = mFiles.Length(); i < count; ++i) { + // We don't want to calculate the size of files and so on, because are + // mainly file descriptors. + size += sizeof(uint64_t); + } + + return size; +} + +inline StructuredCloneReadInfoChild::StructuredCloneReadInfoChild( + JSStructuredCloneData&& aData, nsTArray<StructuredCloneFileChild> aFiles, + IDBDatabase* aDatabase) + : StructuredCloneReadInfo{std::move(aData), std::move(aFiles)}, + mDatabase{aDatabase} {} + +template <typename E, typename Map> +RefPtr<DOMStringList> CreateSortedDOMStringList(const nsTArray<E>& aArray, + const Map& aMap) { + auto list = MakeRefPtr<DOMStringList>(); + + if (!aArray.IsEmpty()) { + nsTArray<nsString>& mapped = list->StringArray(); + mapped.SetCapacity(aArray.Length()); + + std::transform(aArray.cbegin(), aArray.cend(), MakeBackInserter(mapped), + aMap); + + mapped.Sort(); + } + + return list; +} + +template <typename StructuredCloneReadInfoType> +JSObject* StructuredCloneReadCallback( + JSContext* const aCx, JSStructuredCloneReader* const aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, const uint32_t aTag, + const uint32_t aData, void* const aClosure) { + auto* const database = [aClosure]() -> IDBDatabase* { + if constexpr (std::is_same_v<StructuredCloneReadInfoType, + StructuredCloneReadInfoChild>) { + return static_cast<StructuredCloneReadInfoChild*>(aClosure)->Database(); + } + Unused << aClosure; + return nullptr; + }(); + return CommonStructuredCloneReadCallback( + aCx, aReader, aCloneDataPolicy, aTag, aData, + static_cast<StructuredCloneReadInfoType*>(aClosure), database); +} + +template <typename T> +bool WrapAsJSObject(JSContext* const aCx, T& aBaseObject, + JS::MutableHandle<JSObject*> aResult) { + JS::Rooted<JS::Value> wrappedValue(aCx); + if (!ToJSValue(aCx, aBaseObject, &wrappedValue)) { + return false; + } + + aResult.set(&wrappedValue.toObject()); + return true; +} + +} // namespace mozilla::dom::indexedDB + +#endif // IndexedDatabaseInlines_h diff --git a/dom/indexedDB/IndexedDatabaseManager.cpp b/dom/indexedDB/IndexedDatabaseManager.cpp new file mode 100644 index 0000000000..0e4b1f25ad --- /dev/null +++ b/dom/indexedDB/IndexedDatabaseManager.cpp @@ -0,0 +1,759 @@ +/* -*- 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 "nsIScriptError.h" +#include "nsIScriptGlobalObject.h" + +#include "jsapi.h" +#include "js/Object.h" // JS::GetClass +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Preferences.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/intl/LocaleCanonicalizer.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "mozilla/Logging.h" + +#include "ActorsChild.h" +#include "DatabaseFileManager.h" +#include "IDBEvents.h" +#include "IDBFactory.h" +#include "IDBKeyRange.h" +#include "IDBRequest.h" +#include "IndexedDBCommon.h" +#include "ProfilerHelpers.h" +#include "ScriptErrorHelper.h" +#include "nsCharSeparatedTokenizer.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/IDBObjectStoreBinding.h" +#include "mozilla/dom/IDBOpenDBRequestBinding.h" +#include "mozilla/dom/IDBRequestBinding.h" +#include "mozilla/dom/IDBTransactionBinding.h" +#include "mozilla/dom/IDBVersionChangeEventBinding.h" + +#define IDB_STR "indexedDB" + +namespace mozilla::dom { +namespace indexedDB { + +using namespace mozilla::dom::quota; +using namespace mozilla::ipc; + +class FileManagerInfo { + public: + [[nodiscard]] SafeRefPtr<DatabaseFileManager> GetFileManager( + PersistenceType aPersistenceType, const nsAString& aName) const; + + void AddFileManager(SafeRefPtr<DatabaseFileManager> aFileManager); + + bool HasFileManagers() const { + AssertIsOnIOThread(); + + return !mPersistentStorageFileManagers.IsEmpty() || + !mTemporaryStorageFileManagers.IsEmpty() || + !mDefaultStorageFileManagers.IsEmpty() || + !mPrivateStorageFileManagers.IsEmpty(); + } + + void InvalidateAllFileManagers() const; + + void InvalidateAndRemoveFileManagers(PersistenceType aPersistenceType); + + void InvalidateAndRemoveFileManager(PersistenceType aPersistenceType, + const nsAString& aName); + + private: + nsTArray<SafeRefPtr<DatabaseFileManager> >& GetArray( + PersistenceType aPersistenceType); + + const nsTArray<SafeRefPtr<DatabaseFileManager> >& GetImmutableArray( + PersistenceType aPersistenceType) const { + return const_cast<FileManagerInfo*>(this)->GetArray(aPersistenceType); + } + + nsTArray<SafeRefPtr<DatabaseFileManager> > mPersistentStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager> > mTemporaryStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager> > mDefaultStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager> > mPrivateStorageFileManagers; +}; + +} // namespace indexedDB + +using namespace mozilla::dom::indexedDB; + +namespace { + +// 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; + +// The maximum number of records to preload (in addition to the one requested by +// the child). +// +// TODO: The current number was chosen for no particular reason. Telemetry +// should be added to determine whether this is a reasonable number for an +// overwhelming majority of cases. +const int32_t kDefaultMaxPreloadExtraRecords = 64; + +#define IDB_PREF_BRANCH_ROOT "dom.indexedDB." + +const char kDataThresholdPref[] = IDB_PREF_BRANCH_ROOT "dataThreshold"; +const char kPrefMaxSerilizedMsgSize[] = + IDB_PREF_BRANCH_ROOT "maxSerializedMsgSize"; +const char kPrefMaxPreloadExtraRecords[] = + IDB_PREF_BRANCH_ROOT "maxPreloadExtraRecords"; + +#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"; + +const char kPrefLoggingProfiler[] = + IDB_PREF_LOGGING_BRANCH_ROOT "profiler-marks"; + +#undef IDB_PREF_LOGGING_BRANCH_ROOT +#undef IDB_PREF_BRANCH_ROOT + +StaticMutex gDBManagerMutex; +StaticRefPtr<IndexedDatabaseManager> gDBManager MOZ_GUARDED_BY(gDBManagerMutex); +bool gInitialized MOZ_GUARDED_BY(gDBManagerMutex) = false; +bool gClosed MOZ_GUARDED_BY(gDBManagerMutex) = false; + +Atomic<int32_t> gDataThresholdBytes(0); +Atomic<int32_t> gMaxSerializedMsgSize(0); +Atomic<int32_t> gMaxPreloadExtraRecords(0); + +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); +} + +void MaxPreloadExtraRecordsPrefChangeCallback(const char* aPrefName, + void* aClosure) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kPrefMaxPreloadExtraRecords)); + MOZ_ASSERT(!aClosure); + + gMaxPreloadExtraRecords = + Preferences::GetInt(aPrefName, kDefaultMaxPreloadExtraRecords); + MOZ_ASSERT(gMaxPreloadExtraRecords >= 0); + + // TODO: We could also allow setting a negative value to preload all available + // records, but this doesn't seem to be too useful in general, and it would + // require adaptations in ActorsParent.cpp +} + +auto DatabaseNameMatchPredicate(const nsAString* const aName) { + MOZ_ASSERT(aName); + return [aName](const auto& fileManager) { + return fileManager->DatabaseName() == *aName; + }; +} + +} // namespace + +IndexedDatabaseManager::IndexedDatabaseManager() : 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); + +// static +IndexedDatabaseManager* IndexedDatabaseManager::GetOrCreate() { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + StaticMutexAutoLock lock(gDBManagerMutex); + + if (gClosed) { + NS_ERROR("Calling GetOrCreate() after shutdown!"); + return nullptr; + } + + if (!gDBManager) { + sIsMainProcess = XRE_IsParentProcess(); + + if (gInitialized) { + NS_ERROR("Initialized more than once?!"); + } + + RefPtr<IndexedDatabaseManager> instance(new IndexedDatabaseManager()); + + { + StaticMutexAutoUnlock unlock(gDBManagerMutex); + + QM_TRY(MOZ_TO_RESULT(instance->Init()), nullptr); + } + + gDBManager = instance; + + ClearOnShutdown(&gDBManager); + + gInitialized = true; + } + + return gDBManager; +} + +// static +IndexedDatabaseManager* IndexedDatabaseManager::Get() { + StaticMutexAutoLock lock(gDBManagerMutex); + + // Does not return an owning reference. + return gDBManager; +} + +nsresult IndexedDatabaseManager::Init() { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + // 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); + + Preferences::RegisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingProfiler); + + Preferences::RegisterCallbackAndCall(LoggingModePrefChangedCallback, + kPrefLoggingEnabled); + + Preferences::RegisterCallbackAndCall(DataThresholdPrefChangedCallback, + kDataThresholdPref); + + Preferences::RegisterCallbackAndCall(MaxSerializedMsgSizePrefChangeCallback, + kPrefMaxSerilizedMsgSize); + + Preferences::RegisterCallbackAndCall(MaxPreloadExtraRecordsPrefChangeCallback, + kPrefMaxPreloadExtraRecords); + + nsAutoCString acceptLang; + Preferences::GetLocalizedCString("intl.accept_languages", acceptLang); + + // Split values on commas. + for (const auto& lang : + nsCCharSeparatedTokenizer(acceptLang, ',').ToRange()) { + mozilla::intl::LocaleCanonicalizer::Vector asciiString{}; + auto result = mozilla::intl::LocaleCanonicalizer::CanonicalizeICULevel1( + PromiseFlatCString(lang).get(), asciiString); + if (result.isOk()) { + mLocale.AssignASCII(asciiString); + break; + } + } + + if (mLocale.IsEmpty()) { + mLocale.AssignLiteral("en_US"); + } + + return NS_OK; +} + +void IndexedDatabaseManager::Destroy() { + { + StaticMutexAutoLock lock(gDBManagerMutex); + + // 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) { + NS_ERROR("Shutdown more than once?!"); + } + + gClosed = true; + } + + Preferences::UnregisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingDetails); + + Preferences::UnregisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingProfiler); + + Preferences::UnregisterCallback(LoggingModePrefChangedCallback, + kPrefLoggingEnabled); + + Preferences::UnregisterCallback(DataThresholdPrefChangedCallback, + kDataThresholdPref); + + Preferences::UnregisterCallback(MaxSerializedMsgSizePrefChangeCallback, + kPrefMaxSerilizedMsgSize); + + delete this; +} + +// static +bool IndexedDatabaseManager::ResolveSandboxBinding(JSContext* aCx) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT( + JS::GetClass(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 (!IDBCursor_Binding::GetConstructorObject(aCx) || + !IDBCursorWithValue_Binding::GetConstructorObject(aCx) || + !IDBDatabase_Binding::GetConstructorObject(aCx) || + !IDBFactory_Binding::GetConstructorObject(aCx) || + !IDBIndex_Binding::GetConstructorObject(aCx) || + !IDBKeyRange_Binding::GetConstructorObject(aCx) || + !IDBLocaleAwareKeyRange_Binding::GetConstructorObject(aCx) || + !IDBObjectStore_Binding::GetConstructorObject(aCx) || + !IDBOpenDBRequest_Binding::GetConstructorObject(aCx) || + !IDBRequest_Binding::GetConstructorObject(aCx) || + !IDBTransaction_Binding::GetConstructorObject(aCx) || + !IDBVersionChangeEvent_Binding::GetConstructorObject(aCx)) { + return false; + } + + return true; +} + +// static +bool IndexedDatabaseManager::DefineIndexedDB(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL, + "Passed object is not a global object!"); + + nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!global)) { + return false; + } + + QM_TRY_UNWRAP(auto factory, IDBFactory::CreateForMainThreadJS(global), 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() { + StaticMutexAutoLock lock(gDBManagerMutex); + + return gClosed; +} + +#ifdef DEBUG +// static +bool IndexedDatabaseManager::IsMainProcess() { + NS_ASSERTION(Get(), + "IsMainProcess() called before indexedDB has been initialized!"); + NS_ASSERTION((XRE_IsParentProcess()) == sIsMainProcess, + "XRE_GetProcessType changed its tune!"); + return sIsMainProcess; +} + +// static +IndexedDatabaseManager::LoggingMode IndexedDatabaseManager::GetLoggingMode() { + MOZ_ASSERT(Get(), + "GetLoggingMode called before IndexedDatabaseManager has been " + "initialized!"); + + return sLoggingMode; +} + +// static +mozilla::LogModule* IndexedDatabaseManager::GetLoggingModule() { + MOZ_ASSERT(Get(), + "GetLoggingModule called before IndexedDatabaseManager has been " + "initialized!"); + + return sLoggingModule; +} + +#endif // DEBUG + +// static +bool IndexedDatabaseManager::FullSynchronous() { + MOZ_ASSERT(Get(), + "FullSynchronous() called before indexedDB has been initialized!"); + + return sFullSynchronousMode; +} + +// static +uint32_t IndexedDatabaseManager::DataThreshold() { + MOZ_ASSERT(Get(), + "DataThreshold() called before indexedDB has been initialized!"); + + return gDataThresholdBytes; +} + +// static +uint32_t IndexedDatabaseManager::MaxSerializedMsgSize() { + MOZ_ASSERT( + Get(), + "MaxSerializedMsgSize() called before indexedDB has been initialized!"); + MOZ_ASSERT(gMaxSerializedMsgSize > 0); + + return gMaxSerializedMsgSize; +} + +// static +int32_t IndexedDatabaseManager::MaxPreloadExtraRecords() { + MOZ_ASSERT(Get(), + "MaxPreloadExtraRecords() called before indexedDB has been " + "initialized!"); + + return gMaxPreloadExtraRecords; +} + +void IndexedDatabaseManager::ClearBackgroundActor() { + MOZ_ASSERT(NS_IsMainThread()); + + mBackgroundActor = nullptr; +} + +SafeRefPtr<DatabaseFileManager> IndexedDatabaseManager::GetFileManager( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseName) { + AssertIsOnIOThread(); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aOrigin, &info)) { + return nullptr; + } + + return info->GetFileManager(aPersistenceType, aDatabaseName); +} + +void IndexedDatabaseManager::AddFileManager( + SafeRefPtr<DatabaseFileManager> aFileManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFileManager); + + const auto& origin = aFileManager->Origin(); + mFileManagerInfos.GetOrInsertNew(origin)->AddFileManager( + std::move(aFileManager)); +} + +void IndexedDatabaseManager::InvalidateAllFileManagers() { + AssertIsOnIOThread(); + + for (const auto& fileManagerInfo : mFileManagerInfos.Values()) { + fileManagerInfo->InvalidateAllFileManagers(); + } + + mFileManagerInfos.Clear(); +} + +void IndexedDatabaseManager::InvalidateFileManagers( + PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + for (auto iter = mFileManagerInfos.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->InvalidateAndRemoveFileManagers(aPersistenceType); + + if (!iter.Data()->HasFileManagers()) { + iter.Remove(); + } + } +} + +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::BlockAndGetFileReferences( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseName, int64_t aFileId, int32_t* aRefCnt, + int32_t* aDBRefCnt, bool* aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!StaticPrefs::dom_indexedDB_testing())) { + return NS_ERROR_UNEXPECTED; + } + + if (!mBackgroundActor) { + PBackgroundChild* bgActor = BackgroundChild::GetForCurrentThread(); + if (NS_WARN_IF(!bgActor)) { + return NS_ERROR_FAILURE; + } + + BackgroundUtilsChild* actor = new BackgroundUtilsChild(this); + + // We don't set event target for BackgroundUtilsChild because: + // 1. BackgroundUtilsChild is a singleton. + // 2. SendGetFileReferences is a sync operation to be returned asap if + // unlabeled. + // 3. The rest operations like DeleteMe/__delete__ only happens at shutdown. + // Hence, we should keep it unlabeled. + 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, aResult)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult IndexedDatabaseManager::FlushPendingFileDeletions() { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!StaticPrefs::dom_indexedDB_testing())) { + return NS_ERROR_UNEXPECTED; + } + + 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 = Preferences::GetBool(kPrefLoggingProfiler); +#if !defined(MOZ_GECKO_PROFILER) + if (useProfiler) { + NS_WARNING( + "IndexedDB cannot create profiler marks because this build does " + "not have profiler extensions enabled!"); + useProfiler = false; + } +#endif + + const bool logDetails = Preferences::GetBool(kPrefLoggingDetails); + + if (useProfiler) { + sLoggingMode = logDetails ? Logging_DetailedProfilerMarks + : Logging_ConciseProfilerMarks; + } else { + sLoggingMode = logDetails ? Logging_Detailed : Logging_Concise; + } +} + +// static +const nsCString& IndexedDatabaseManager::GetLocale() { + IndexedDatabaseManager* idbManager = Get(); + MOZ_ASSERT(idbManager, "IDBManager is not ready!"); + + return idbManager->mLocale; +} + +SafeRefPtr<DatabaseFileManager> FileManagerInfo::GetFileManager( + PersistenceType aPersistenceType, const nsAString& aName) const { + AssertIsOnIOThread(); + + const auto& managers = GetImmutableArray(aPersistenceType); + + const auto end = managers.cend(); + const auto foundIt = + std::find_if(managers.cbegin(), end, DatabaseNameMatchPredicate(&aName)); + + return foundIt != end ? foundIt->clonePtr() : nullptr; +} + +void FileManagerInfo::AddFileManager( + SafeRefPtr<DatabaseFileManager> aFileManager) { + AssertIsOnIOThread(); + + nsTArray<SafeRefPtr<DatabaseFileManager> >& managers = + GetArray(aFileManager->Type()); + + NS_ASSERTION(!managers.Contains(aFileManager), "Adding more than once?!"); + + managers.AppendElement(std::move(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(); + } + + for (i = 0; i < mPrivateStorageFileManagers.Length(); i++) { + mPrivateStorageFileManagers[i]->Invalidate(); + } +} + +void FileManagerInfo::InvalidateAndRemoveFileManagers( + PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + nsTArray<SafeRefPtr<DatabaseFileManager> >& 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(); + + auto& managers = GetArray(aPersistenceType); + const auto end = managers.cend(); + const auto foundIt = + std::find_if(managers.cbegin(), end, DatabaseNameMatchPredicate(&aName)); + + if (foundIt != end) { + (*foundIt)->Invalidate(); + managers.RemoveElementAt(foundIt.GetIndex()); + } +} + +nsTArray<SafeRefPtr<DatabaseFileManager> >& 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_PRIVATE: + return mPrivateStorageFileManagers; + + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad storage type value!"); + } +} + +} // namespace mozilla::dom diff --git a/dom/indexedDB/IndexedDatabaseManager.h b/dom/indexedDB/IndexedDatabaseManager.h new file mode 100644 index 0000000000..5bf6485aff --- /dev/null +++ b/dom/indexedDB/IndexedDatabaseManager.h @@ -0,0 +1,163 @@ +/* -*- 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 "js/TypeDecls.h" +#include "mozilla/Atomics.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/Logging.h" +#include "mozilla/Mutex.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "SafeRefPtr.h" + +namespace mozilla { + +class EventChainPostVisitor; + +namespace dom { + +class IDBFactory; + +namespace indexedDB { + +class BackgroundUtilsChild; +class DatabaseFileManager; +class FileManagerInfo; + +} // namespace indexedDB + +class IndexedDatabaseManager final { + using PersistenceType = mozilla::dom::quota::PersistenceType; + using DatabaseFileManager = mozilla::dom::indexedDB::DatabaseFileManager; + using FileManagerInfo = mozilla::dom::indexedDB::FileManagerInfo; + + public: + enum LoggingMode { + Logging_Disabled = 0, + Logging_Concise, + Logging_Detailed, + Logging_ConciseProfilerMarks, + Logging_DetailedProfilerMarks + }; + + NS_INLINE_DECL_REFCOUNTING_WITH_DESTROY(IndexedDatabaseManager, Destroy()) + + // 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 FullSynchronous(); + + static LoggingMode GetLoggingMode() +#ifdef DEBUG + ; +#else + { + return sLoggingMode; + } +#endif + + static mozilla::LogModule* GetLoggingModule() +#ifdef DEBUG + ; +#else + { + return sLoggingModule; + } +#endif + + static uint32_t DataThreshold(); + + static uint32_t MaxSerializedMsgSize(); + + // The maximum number of extra entries to preload in an Cursor::OpenOp or + // Cursor::ContinueOp. + static int32_t MaxPreloadExtraRecords(); + + void ClearBackgroundActor(); + + [[nodiscard]] SafeRefPtr<DatabaseFileManager> GetFileManager( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseName); + + void AddFileManager(SafeRefPtr<DatabaseFileManager> aFileManager); + + void InvalidateAllFileManagers(); + + void InvalidateFileManagers(PersistenceType aPersistenceType); + + void InvalidateFileManagers(PersistenceType aPersistenceType, + const nsACString& aOrigin); + + void InvalidateFileManager(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseName); + + // 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, bool* aResult); + + nsresult FlushPendingFileDeletions(); + + static const nsCString& GetLocale(); + + 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); + + // Maintains a list of all DatabaseFileManager objects 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<DatabaseFileManager>, nsTArray<int64_t>> + mPendingDeleteInfos; + + nsCString mLocale; + + indexedDB::BackgroundUtilsChild* mBackgroundActor; + + static bool sIsMainProcess; + static bool sFullSynchronousMode; + static LazyLogModule sLoggingModule; + static Atomic<LoggingMode> sLoggingMode; +}; + +} // 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 0000000000..8b82e70e37 --- /dev/null +++ b/dom/indexedDB/Key.cpp @@ -0,0 +1,962 @@ +/* -*- 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 <cstdint> +#include <stdint.h> // for UINT32_MAX, uintptr_t +#include "js/Array.h" // JS::NewArrayObject +#include "js/ArrayBuffer.h" // JS::{IsArrayBufferObject,NewArrayBuffer{,WithContents},GetArrayBufferLengthAndData} +#include "js/Date.h" +#include "js/experimental/TypedData.h" // JS_IsArrayBufferViewObject, JS_GetObjectAsArrayBufferView +#include "js/MemoryFunctions.h" +#include "js/Object.h" // JS::GetBuiltinClass +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_HasOwnPropertyById +#include "js/Value.h" +#include "jsfriendapi.h" +#include "mozilla/Casting.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ReverseIterator.h" +#include "mozilla/dom/indexedDB/IDBResult.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "nsJSUtils.h" +#include "nsTStringRepr.h" +#include "ReportInternalError.h" +#include "xpcpublic.h" + +namespace mozilla::dom::indexedDB { + +namespace { +// Implementation of the array branch of step 3 of +// https://w3c.github.io/IndexedDB/#convert-value-to-key +template <typename ArrayConversionPolicy> +IDBResult<Ok, IDBSpecialValue::Invalid> ConvertArrayValueToKey( + JSContext* const aCx, JS::Handle<JSObject*> aObject, + ArrayConversionPolicy&& aPolicy) { + // 1. Let `len` be ? ToLength( ? Get(`input`, "length")). + uint32_t len; + if (!JS::GetArrayLength(aCx, aObject, &len)) { + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + // 2. Add `input` to `seen`. + aPolicy.AddToSeenSet(aCx, aObject); + + // 3. Let `keys` be a new empty list. + aPolicy.BeginSubkeyList(); + + // 4. Let `index` be 0. + uint32_t index = 0; + + // 5. While `index` is less than `len`: + while (index < len) { + JS::Rooted<JS::PropertyKey> indexId(aCx); + if (!JS_IndexToId(aCx, index, &indexId)) { + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + // 1. Let `hop` be ? HasOwnProperty(`input`, `index`). + bool hop; + if (!JS_HasOwnPropertyById(aCx, aObject, indexId, &hop)) { + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + // 2. If `hop` is false, return invalid. + if (!hop) { + return Err(IDBError(SpecialValues::Invalid)); + } + + // 3. Let `entry` be ? Get(`input`, `index`). + JS::Rooted<JS::Value> entry(aCx); + if (!JS_GetPropertyById(aCx, aObject, indexId, &entry)) { + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + // 4. Let `key` be the result of running the steps to convert a value to a + // key with arguments `entry` and `seen`. + // 5. ReturnIfAbrupt(`key`). + // 6. If `key` is invalid abort these steps and return invalid. + // 7. Append `key` to `keys`. + auto result = aPolicy.ConvertSubkey(aCx, entry, index); + if (result.isErr()) { + return result; + } + + // 8. Increase `index` by 1. + index += 1; + } + + // 6. Return a new array key with value `keys`. + aPolicy.EndSubkeyList(); + return Ok(); +} +} // namespace + +/* + 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-strategy 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 +*/ + +Result<Ok, nsresult> Key::SetFromString(const nsAString& aString) { + mBuffer.Truncate(); + auto result = EncodeString(aString, 0); + if (result.isOk()) { + TrimBuffer(); + } + return result; +} + +// |aPos| should point to the type indicator. +// The returned length doesn't include the type indicator +// or the terminator. +// static +uint32_t Key::LengthOfEncodedBinary(const EncodedDataType* aPos, + const EncodedDataType* aEnd) { + MOZ_ASSERT(*aPos % Key::eMaxType == Key::eBinary, "Don't call me!"); + + const auto* iter = aPos + 1; + for (; iter < aEnd && *iter != eTerminator; ++iter) { + if (*iter & 0x80) { + ++iter; + // XXX if iter == aEnd now, we got a bad enconding, should we report that + // also in non-debug builds? + MOZ_ASSERT(iter < aEnd); + } + } + + return iter - aPos - 1; +} + +Result<Key, nsresult> Key::ToLocaleAwareKey(const nsCString& aLocale) const { + Key res; + + if (IsUnset()) { + return res; + } + + if (IsFloat() || IsDate() || IsBinary()) { + res.mBuffer = mBuffer; + return res; + } + + auto* it = BufferStart(); + auto* const end = BufferEnd(); + + // 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) { + const auto type = *it % eMaxType; + if (type == eTerminator) { + it++; + } else if (type == eFloat || type == eDate) { + it++; + it += std::min(sizeof(uint64_t), size_t(end - it)); + } else if (type == eBinary) { + // skip all binary data + const auto binaryLength = LengthOfEncodedBinary(it, end); + it++; + it += binaryLength; + } else { + // We have a string! + canShareBuffers = false; + break; + } + } + + if (canShareBuffers) { + MOZ_ASSERT(it == end); + res.mBuffer = mBuffer; + return res; + } + + if (!res.mBuffer.SetCapacity(mBuffer.Length(), fallible)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + // A string was found, so we need to copy the data we've read so far + auto* const start = BufferStart(); + if (it > start) { + char* buffer; + MOZ_ALWAYS_TRUE(res.mBuffer.GetMutableData(&buffer, it - start)); + std::copy(start, it, buffer); + } + + // Now continue decoding + while (it < end) { + char* buffer; + const size_t oldLen = res.mBuffer.Length(); + const auto type = *it % eMaxType; + + // Note: Do not modify |it| before calling |updateBufferAndIter|; + // |byteCount| doesn't include the type indicator + const auto updateBufferAndIter = [&](size_t byteCount) -> bool { + if (!res.mBuffer.GetMutableData(&buffer, oldLen + 1 + byteCount)) { + return false; + } + buffer += oldLen; + + // should also copy the type indicator at the begining + std::copy_n(it, byteCount + 1, buffer); + it += (byteCount + 1); + return true; + }; + + if (type == eTerminator) { + // Copy array TypeID and terminator from raw key + if (!updateBufferAndIter(0)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + } else if (type == eFloat || type == eDate) { + // Copy number from raw key + const size_t byteCount = std::min(sizeof(uint64_t), size_t(end - it - 1)); + + if (!updateBufferAndIter(byteCount)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + } else if (type == eBinary) { + // skip all binary data + const auto binaryLength = LengthOfEncodedBinary(it, end); + + if (!updateBufferAndIter(binaryLength)) { + return Err(NS_ERROR_OUT_OF_MEMORY); + } + } else { + // Decode string and reencode + const uint8_t typeOffset = *it - eString; + MOZ_ASSERT((typeOffset % eArray == 0) && (typeOffset / eArray <= 2)); + + auto str = DecodeString(it, end); + auto result = res.EncodeLocaleString(str, typeOffset, aLocale); + if (NS_WARN_IF(result.isErr())) { + return result.propagateErr(); + } + } + } + res.TrimBuffer(); + return res; +} + +class MOZ_STACK_CLASS Key::ArrayValueEncoder final { + public: + ArrayValueEncoder(Key& aKey, const uint8_t aTypeOffset, + const uint16_t aRecursionDepth) + : mKey(aKey), + mTypeOffset(aTypeOffset), + mRecursionDepth(aRecursionDepth) {} + + void AddToSeenSet(JSContext* const aCx, JS::Handle<JSObject*>) { + ++mRecursionDepth; + } + + void BeginSubkeyList() { + mTypeOffset += Key::eMaxType; + if (mTypeOffset == eMaxType * kMaxArrayCollapse) { + mKey.mBuffer.Append(mTypeOffset); + mTypeOffset = 0; + } + MOZ_ASSERT(mTypeOffset % eMaxType == 0, + "Current type offset must indicate beginning of array"); + MOZ_ASSERT(mTypeOffset < eMaxType * kMaxArrayCollapse); + } + + IDBResult<Ok, IDBSpecialValue::Invalid> ConvertSubkey( + JSContext* const aCx, JS::Handle<JS::Value> aEntry, + const uint32_t aIndex) { + auto result = + mKey.EncodeJSValInternal(aCx, aEntry, mTypeOffset, mRecursionDepth); + mTypeOffset = 0; + return result; + } + + void EndSubkeyList() const { mKey.mBuffer.Append(eTerminator + mTypeOffset); } + + private: + Key& mKey; + uint8_t mTypeOffset; + uint16_t mRecursionDepth; +}; + +// Implements the following algorithm: +// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key +IDBResult<Ok, IDBSpecialValue::Invalid> Key::EncodeJSValInternal( + JSContext* const aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset, + const uint16_t aRecursionDepth) { + static_assert(eMaxType * kMaxArrayCollapse < 256, "Unable to encode jsvals."); + + // 1. If `seen` was not given, let `seen` be a new empty set. + // 2. If `input` is in `seen` return invalid. + // Note: we replace this check with a simple recursion depth check. + if (NS_WARN_IF(aRecursionDepth == kMaxRecursionDepth)) { + return Err(IDBError(SpecialValues::Invalid)); + } + + // 3. Jump to the appropriate step below: + // Note: some cases appear out of order to make the implementation more + // straightforward. This shouldn't affect observable behavior. + + // If Type(`input`) is Number + if (aVal.isNumber()) { + const auto number = aVal.toNumber(); + + // 1. If `input` is NaN then return invalid. + if (std::isnan(number)) { + return Err(IDBError(SpecialValues::Invalid)); + } + + // 2. Otherwise, return a new key with type `number` and value `input`. + return EncodeNumber(number, eFloat + aTypeOffset); + } + + // If Type(`input`) is String + if (aVal.isString()) { + // 1. Return a new key with type `string` and value `input`. + nsAutoJSString string; + if (!string.init(aCx, aVal)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + return EncodeString(string, aTypeOffset); + } + + if (aVal.isObject()) { + JS::Rooted<JSObject*> object(aCx, &aVal.toObject()); + + js::ESClass builtinClass; + if (!JS::GetBuiltinClass(aCx, object, &builtinClass)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + // If `input` is a Date (has a [[DateValue]] internal slot) + if (builtinClass == js::ESClass::Date) { + // 1. Let `ms` be the value of `input`’s [[DateValue]] internal slot. + double ms; + if (!js::DateGetMsecSinceEpoch(aCx, object, &ms)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(IDBException(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR)); + } + + // 2. If `ms` is NaN then return invalid. + if (std::isnan(ms)) { + return Err(IDBError(SpecialValues::Invalid)); + } + + // 3. Otherwise, return a new key with type `date` and value `ms`. + return EncodeNumber(ms, eDate + aTypeOffset); + } + + // If `input` is a buffer source type + if (JS::IsArrayBufferObject(object) || JS_IsArrayBufferViewObject(object)) { + const bool isViewObject = JS_IsArrayBufferViewObject(object); + return EncodeBinary(object, isViewObject, aTypeOffset); + } + + // If IsArray(`input`) + if (builtinClass == js::ESClass::Array) { + return ConvertArrayValueToKey( + aCx, object, ArrayValueEncoder{*this, aTypeOffset, aRecursionDepth}); + } + } + + // Otherwise + // Return invalid. + return Err(IDBError(SpecialValues::Invalid)); +} + +// static +nsresult Key::DecodeJSValInternal(const EncodedDataType*& aPos, + const EncodedDataType* 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) { + QM_TRY(MOZ_TO_RESULT(DecodeJSValInternal(aPos, aEnd, aCx, aTypeOffset, + &val, aRecursionDepth + 1))); + + 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) { + auto key = DecodeString(aPos, aEnd); + 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 { + MOZ_ASSERT_UNREACHABLE("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 + +IDBResult<Ok, IDBSpecialValue::Invalid> Key::EncodeJSVal( + JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset) { + return EncodeJSValInternal(aCx, aVal, aTypeOffset, 0); +} + +Result<Ok, nsresult> Key::EncodeString(const nsAString& aString, + uint8_t aTypeOffset) { + return EncodeString(Span{aString}, aTypeOffset); +} + +template <typename T> +Result<Ok, nsresult> Key::EncodeString(const Span<const T> aInput, + uint8_t aTypeOffset) { + return EncodeAsString(aInput, eString + aTypeOffset); +} + +// nsCString maximum length is limited by INT32_MAX. +// XXX: We probably want to enforce even shorter keys, though. +#define KEY_MAXIMUM_BUFFER_LENGTH \ + ::mozilla::detail::nsTStringLengthStorage<char>::kMax + +template <typename T> +Result<Ok, nsresult> Key::EncodeAsString(const Span<const T> aInput, + uint8_t aType) { + // Please note that the input buffer can either be based on two-byte UTF-16 + // values or on arbitrary single byte binary values. Only the first case + // needs to account for the TWO_BYTE_LIMIT of UTF-8. + // First we measure how long the encoded string will be. + + // The 2 is for initial aType and trailing 0. We'll compensate for multi-byte + // chars below. + size_t size = 2; + + // We construct a range over the raw pointers here because this loop is + // time-critical. + // XXX It might be good to encapsulate this in some function to make it less + // error-prone and more expressive. + const auto inputRange = mozilla::detail::IteratorRange( + aInput.Elements(), aInput.Elements() + aInput.Length()); + + size_t payloadSize = aInput.Length(); + bool anyMultibyte = false; + for (const T val : inputRange) { + if (val > ONE_BYTE_LIMIT) { + anyMultibyte = true; + payloadSize += char16_t(val) > TWO_BYTE_LIMIT ? 2 : 1; + if (payloadSize > KEY_MAXIMUM_BUFFER_LENGTH) { + return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); + } + } + } + + size += payloadSize; + + // Now we allocate memory for the new size + size_t oldLen = mBuffer.Length(); + size += oldLen; + + if (size > KEY_MAXIMUM_BUFFER_LENGTH) { + return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); + } + + char* buffer; + if (!mBuffer.GetMutableData(&buffer, size)) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + } + buffer += oldLen; + + // Write type marker + *(buffer++) = aType; + + // Encode string + if (anyMultibyte) { + for (const auto val : inputRange) { + if (val <= ONE_BYTE_LIMIT) { + *(buffer++) = val + ONE_BYTE_ADJUST; + } else if (char16_t(val) <= TWO_BYTE_LIMIT) { + char16_t c = char16_t(val) + TWO_BYTE_ADJUST + 0x8000; + *(buffer++) = (char)(c >> 8); + *(buffer++) = (char)(c & 0xFF); + } else { + uint32_t c = (uint32_t(val) << THREE_BYTE_SHIFT) | 0x00C00000; + *(buffer++) = (char)(c >> 16); + *(buffer++) = (char)(c >> 8); + *(buffer++) = (char)c; + } + } + } else { + // Optimization for the case where there are no multibyte characters. + // This is ca. 13 resp. 5.8 times faster than the non-optimized version in + // an -O2 build: https://quick-bench.com/q/v1oBpLGifs-3w_pkZG8alVSWVAw, for + // the T==uint8_t resp. T==char16_t cases (for the char16_t case, copying + // and then adjusting could even be slightly faster, but then we would need + // another case distinction here) + size_t inputLen = std::distance(inputRange.cbegin(), inputRange.cend()); + MOZ_ASSERT(inputLen == payloadSize); + std::transform(inputRange.cbegin(), inputRange.cend(), buffer, + [](auto value) { return value + ONE_BYTE_ADJUST; }); + buffer += inputLen; + } + + // Write end marker + *(buffer++) = eTerminator; + + NS_ASSERTION(buffer == mBuffer.EndReading(), "Wrote wrong number of bytes"); + + return Ok(); +} + +Result<Ok, nsresult> Key::EncodeLocaleString(const nsAString& aString, + uint8_t aTypeOffset, + const nsCString& aLocale) { + const int length = aString.Length(); + if (length == 0) { + return Ok(); + } + + auto collResult = intl::Collator::TryCreate(aLocale.get()); + if (collResult.isErr()) { + return Err(NS_ERROR_FAILURE); + } + auto collator = collResult.unwrap(); + MOZ_ASSERT(collator); + + AutoTArray<uint8_t, 128> keyBuffer; + MOZ_TRY(collator->GetSortKey(Span{aString}, keyBuffer) + .mapErr([](intl::ICUError icuError) { + return icuError == intl::ICUError::OutOfMemory + ? NS_ERROR_OUT_OF_MEMORY + : NS_ERROR_FAILURE; + })); + + size_t sortKeyLength = keyBuffer.Length(); + return EncodeString(Span{keyBuffer}.AsConst().First(sortKeyLength), + aTypeOffset); +} + +// static +nsresult Key::DecodeJSVal(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, JSContext* aCx, + JS::MutableHandle<JS::Value> aVal) { + return DecodeJSValInternal(aPos, aEnd, aCx, 0, aVal, 0); +} + +// static +template <typename T> +uint32_t Key::CalcDecodedStringySize( + const EncodedDataType* const aBegin, const EncodedDataType* const aEnd, + const EncodedDataType** aOutEncodedSectionEnd) { + static_assert(sizeof(T) <= 2, + "Only implemented for 1 and 2 byte decoded types"); + uint32_t decodedSize = 0; + auto* iter = aBegin; + for (; iter < aEnd && *iter != eTerminator; ++iter) { + if (*iter & 0x80) { + iter += (sizeof(T) > 1 && (*iter & 0x40)) ? 2 : 1; + } + ++decodedSize; + } + *aOutEncodedSectionEnd = std::min(aEnd, iter); + return decodedSize; +} + +// static +template <typename T> +void Key::DecodeAsStringy(const EncodedDataType* const aEncodedSectionBegin, + const EncodedDataType* const aEncodedSectionEnd, + const uint32_t aDecodedLength, T* const aOut) { + static_assert(sizeof(T) <= 2, + "Only implemented for 1 and 2 byte decoded types"); + T* decodedPos = aOut; + for (const EncodedDataType* iter = aEncodedSectionBegin; + iter < aEncodedSectionEnd;) { + if (!(*iter & 0x80)) { + *decodedPos = *(iter++) - ONE_BYTE_ADJUST; + } else if (sizeof(T) == 1 || !(*iter & 0x40)) { + auto c = static_cast<uint16_t>(*(iter++)) << 8; + if (iter < aEncodedSectionEnd) { + c |= *(iter++); + } + *decodedPos = static_cast<T>(c - TWO_BYTE_ADJUST - 0x8000); + } else if (sizeof(T) > 1) { + auto c = static_cast<uint32_t>(*(iter++)) << (16 - THREE_BYTE_SHIFT); + if (iter < aEncodedSectionEnd) { + c |= static_cast<uint32_t>(*(iter++)) << (8 - THREE_BYTE_SHIFT); + } + if (iter < aEncodedSectionEnd) { + c |= *(iter++) >> THREE_BYTE_SHIFT; + } + *decodedPos = static_cast<T>(c); + } + ++decodedPos; + } + + MOZ_ASSERT(static_cast<uint32_t>(decodedPos - aOut) == aDecodedLength, + "Should have written the whole decoded area"); +} + +// static +template <Key::EncodedDataType TypeMask, typename T, typename AcquireBuffer, + typename AcquireEmpty> +void Key::DecodeStringy(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, + const AcquireBuffer& acquireBuffer, + const AcquireEmpty& acquireEmpty) { + NS_ASSERTION(*aPos % eMaxType == TypeMask, "Don't call me!"); + + // First measure how big the decoded stringy data will be. + const EncodedDataType* const encodedSectionBegin = aPos + 1; + const EncodedDataType* encodedSectionEnd; + // decodedLength does not include the terminating 0 (in case of a string) + const uint32_t decodedLength = + CalcDecodedStringySize<T>(encodedSectionBegin, aEnd, &encodedSectionEnd); + aPos = encodedSectionEnd + 1; + + if (!decodedLength) { + acquireEmpty(); + return; + } + + T* out; + if (!acquireBuffer(&out, decodedLength)) { + return; + } + + DecodeAsStringy(encodedSectionBegin, encodedSectionEnd, decodedLength, out); +} + +// static +nsAutoString Key::DecodeString(const EncodedDataType*& aPos, + const EncodedDataType* const aEnd) { + nsAutoString res; + DecodeStringy<eString, char16_t>( + aPos, aEnd, + [&res](char16_t** out, uint32_t decodedLength) { + return 0 != res.GetMutableData(out, decodedLength); + }, + [] {}); + return res; +} + +Result<Ok, nsresult> Key::EncodeNumber(double aFloat, uint8_t aType) { + // Allocate memory for the new size + size_t oldLen = mBuffer.Length(); + size_t newLen = oldLen + 1 + sizeof(double); + if (newLen > KEY_MAXIMUM_BUFFER_LENGTH) { + return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); + } + + char* buffer; + if (!mBuffer.GetMutableData(&buffer, newLen)) { + return Err(NS_ERROR_DOM_INDEXEDDB_KEY_ERR); + } + 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); + return Ok(); +} + +// static +double Key::DecodeNumber(const EncodedDataType*& aPos, + const EncodedDataType* 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); +} + +Result<Ok, nsresult> Key::EncodeBinary(JSObject* aObject, bool aIsViewObject, + uint8_t aTypeOffset) { + uint8_t* bufferData; + size_t bufferLength; + + // We must use JS::GetObjectAsArrayBuffer()/JS_GetObjectAsArrayBufferView() + // instead of js::GetArrayBufferLengthAndData(). The object might be wrapped, + // the former will handle the wrapped case, the later won't. + if (aIsViewObject) { + bool unused; + JS_GetObjectAsArrayBufferView(aObject, &bufferLength, &unused, &bufferData); + } else { + JS::GetObjectAsArrayBuffer(aObject, &bufferLength, &bufferData); + } + + return EncodeAsString(Span{bufferData, bufferLength}.AsConst(), + eBinary + aTypeOffset); +} + +// static +JSObject* Key::DecodeBinary(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, JSContext* aCx) { + JS::Rooted<JSObject*> rv(aCx); + DecodeStringy<eBinary, uint8_t>( + aPos, aEnd, + [&rv, aCx](uint8_t** out, uint32_t decodedSize) { + *out = static_cast<uint8_t*>(JS_malloc(aCx, decodedSize)); + if (NS_WARN_IF(!*out)) { + rv = nullptr; + return false; + } + rv = JS::NewArrayBufferWithContents(aCx, decodedSize, *out); + return true; + }, + [&rv, aCx] { rv = JS::NewArrayBuffer(aCx, 0); }); + return rv; +} + +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); +} + +IDBResult<Ok, IDBSpecialValue::Invalid> Key::SetFromJSVal( + JSContext* aCx, JS::Handle<JS::Value> aVal) { + mBuffer.Truncate(); + + if (aVal.isNull() || aVal.isUndefined()) { + Unset(); + return Ok(); + } + + auto result = EncodeJSVal(aCx, aVal, 0); + if (result.isErr()) { + Unset(); + return result; + } + TrimBuffer(); + return Ok(); +} + +nsresult Key::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) const { + if (IsUnset()) { + aVal.setUndefined(); + return NS_OK; + } + + const EncodedDataType* 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; +} + +IDBResult<Ok, IDBSpecialValue::Invalid> Key::AppendItem( + JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal) { + auto result = EncodeJSVal(aCx, aVal, aFirstOfArray ? eMaxType : 0); + if (result.isErr()) { + Unset(); + } + return result; +} + +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; +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/Key.h b/dom/indexedDB/Key.h new file mode 100644 index 0000000000..d19bc94e53 --- /dev/null +++ b/dom/indexedDB/Key.h @@ -0,0 +1,280 @@ +/* -*- 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 "mozilla/dom/indexedDB/IDBResult.h" + +class mozIStorageStatement; +class mozIStorageValueArray; + +namespace IPC { + +template <typename> +struct ParamTraits; + +} // namespace IPC + +namespace mozilla::dom::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(nsCString aBuffer) : mBuffer(std::move(aBuffer)) {} + + bool operator==(const Key& aOther) const { + MOZ_ASSERT(!mBuffer.IsVoid()); + MOZ_ASSERT(!aOther.mBuffer.IsVoid()); + + return mBuffer.Equals(aOther.mBuffer); + } + + bool operator!=(const Key& aOther) const { + MOZ_ASSERT(!mBuffer.IsVoid()); + MOZ_ASSERT(!aOther.mBuffer.IsVoid()); + + return !mBuffer.Equals(aOther.mBuffer); + } + + bool operator<(const Key& aOther) const { + MOZ_ASSERT(!mBuffer.IsVoid()); + MOZ_ASSERT(!aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) < 0; + } + + bool operator>(const Key& aOther) const { + MOZ_ASSERT(!mBuffer.IsVoid()); + MOZ_ASSERT(!aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) > 0; + } + + bool operator<=(const Key& aOther) const { + MOZ_ASSERT(!mBuffer.IsVoid()); + MOZ_ASSERT(!aOther.mBuffer.IsVoid()); + + return Compare(mBuffer, aOther.mBuffer) <= 0; + } + + bool operator>=(const Key& aOther) const { + MOZ_ASSERT(!mBuffer.IsVoid()); + MOZ_ASSERT(!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 { + MOZ_ASSERT(IsFloat()); + const EncodedDataType* pos = BufferStart(); + double res = DecodeNumber(pos, BufferEnd()); + MOZ_ASSERT(pos >= BufferEnd()); + return res; + } + + double ToDateMsec() const { + MOZ_ASSERT(IsDate()); + const EncodedDataType* pos = BufferStart(); + double res = DecodeNumber(pos, BufferEnd()); + MOZ_ASSERT(pos >= BufferEnd()); + return res; + } + + nsAutoString ToString() const { + MOZ_ASSERT(IsString()); + const EncodedDataType* pos = BufferStart(); + auto res = DecodeString(pos, BufferEnd()); + MOZ_ASSERT(pos >= BufferEnd()); + return res; + } + + Result<Ok, nsresult> SetFromString(const nsAString& aString); + + Result<Ok, nsresult> SetFromInteger(int64_t aInt) { + mBuffer.Truncate(); + auto ret = EncodeNumber(double(aInt), eFloat); + TrimBuffer(); + return ret; + } + + // This function implements the standard algorithm "convert a value to a key". + // A key return value is indicated by returning `true` whereas `false` means + // either invalid (if `aRv.Failed()` is `false`) or an exception (otherwise). + IDBResult<Ok, IDBSpecialValue::Invalid> 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; + + // See SetFromJSVal() for the meaning of values returned by this function. + IDBResult<Ok, IDBSpecialValue::Invalid> AppendItem( + JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal); + + Result<Key, nsresult> ToLocaleAwareKey(const nsCString& aLocale) const; + + 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: + class MOZ_STACK_CLASS ArrayValueEncoder; + + using EncodedDataType = unsigned char; + + const EncodedDataType* BufferStart() const { + // TODO it would be nicer if mBuffer was also using EncodedDataType + return reinterpret_cast<const EncodedDataType*>(mBuffer.BeginReading()); + } + + const EncodedDataType* BufferEnd() const { + return reinterpret_cast<const EncodedDataType*>(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 + IDBResult<Ok, IDBSpecialValue::Invalid> EncodeJSVal( + JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset); + + Result<Ok, nsresult> EncodeString(const nsAString& aString, + uint8_t aTypeOffset); + + template <typename T> + Result<Ok, nsresult> EncodeString(Span<const T> aInput, uint8_t aTypeOffset); + + template <typename T> + Result<Ok, nsresult> EncodeAsString(Span<const T> aInput, uint8_t aType); + + Result<Ok, nsresult> EncodeLocaleString(const nsAString& aString, + uint8_t aTypeOffset, + const nsCString& aLocale); + + Result<Ok, nsresult> EncodeNumber(double aFloat, uint8_t aType); + + Result<Ok, nsresult> EncodeBinary(JSObject* aObject, bool aIsViewObject, + uint8_t aTypeOffset); + + // Decoding functions. aPos points into mBuffer and is adjusted to point + // past the consumed value. (Note: this may be beyond aEnd). + static nsresult DecodeJSVal(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, JSContext* aCx, + JS::MutableHandle<JS::Value> aVal); + + static nsAutoString DecodeString(const EncodedDataType*& aPos, + const EncodedDataType* aEnd); + + static double DecodeNumber(const EncodedDataType*& aPos, + const EncodedDataType* aEnd); + + static JSObject* DecodeBinary(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, JSContext* aCx); + + // Returns the size of the decoded data for stringy (string or binary), + // excluding a null terminator. + // On return, aOutSectionEnd points to the last byte behind the current + // encoded section, i.e. either aEnd, or the eTerminator. + // T is the base type for the decoded data. + template <typename T> + static uint32_t CalcDecodedStringySize( + const EncodedDataType* aBegin, const EncodedDataType* aEnd, + const EncodedDataType** aOutEncodedSectionEnd); + + static uint32_t LengthOfEncodedBinary(const EncodedDataType* aPos, + const EncodedDataType* aEnd); + + template <typename T> + static void DecodeAsStringy(const EncodedDataType* aEncodedSectionBegin, + const EncodedDataType* aEncodedSectionEnd, + uint32_t aDecodedLength, T* aOut); + + template <EncodedDataType TypeMask, typename T, typename AcquireBuffer, + typename AcquireEmpty> + static void DecodeStringy(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, + const AcquireBuffer& acquireBuffer, + const AcquireEmpty& acquireEmpty); + + IDBResult<Ok, IDBSpecialValue::Invalid> EncodeJSValInternal( + JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset, + uint16_t aRecursionDepth); + + static nsresult DecodeJSValInternal(const EncodedDataType*& aPos, + const EncodedDataType* aEnd, + JSContext* aCx, uint8_t aTypeOffset, + JS::MutableHandle<JS::Value> aVal, + uint16_t aRecursionDepth); + + template <typename T> + nsresult SetFromSource(T* aSource, uint32_t aIndex); +}; + +} // namespace mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_key_h__ diff --git a/dom/indexedDB/KeyPath.cpp b/dom/indexedDB/KeyPath.cpp new file mode 100644 index 0000000000..5cad164296 --- /dev/null +++ b/dom/indexedDB/KeyPath.cpp @@ -0,0 +1,558 @@ +/* -*- 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 "IndexedDBCommon.h" +#include "Key.h" +#include "ReportInternalError.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_DeleteUCProperty +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnUCPropertyDescriptor +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Blob.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/IDBObjectStoreBinding.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "xpcpublic.h" + +namespace mozilla::dom::indexedDB { + +namespace { + +using KeyPathTokenizer = + nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>; + +bool IsValidKeyPathString(const nsAString& aKeyPath) { + NS_ASSERTION(!aKeyPath.IsVoid(), "What?"); + + for (const auto& token : KeyPathTokenizer(aKeyPath, '.').ToRange()) { + if (token.IsEmpty()) { + return false; + } + + if (!JS_IsIdentifier(token.Data(), 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. + return aKeyPath.IsEmpty() || aKeyPath.CharAt(aKeyPath.Length() - 1) != '.'; +} + +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 auto& token = tokenizer.nextToken(); + + NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath"); + + const char16_t* keyPathChars = token.BeginReading(); + const size_t keyPathLen = token.Length(); + + if (!targetObject) { + // We're still walking the chain of existing objects + // http://w3c.github.io/IndexedDB/#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")) { + aKeyJSVal->setNumber(JS_GetStringLength(currentVal.toString())); + break; + } + + if (!currentVal.isObject()) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + obj = ¤tVal.toObject(); + + // We call JS_GetOwnUCPropertyDescriptor on purpose (as opposed to + // JS_GetUCPropertyDescriptor) to avoid searching the prototype chain. + JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(aCx); + QM_TRY(OkIf(JS_GetOwnUCPropertyDescriptor(aCx, obj, keyPathChars, + keyPathLen, &desc)), + NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + + JS::Rooted<JS::Value> intermediate(aCx); + bool hasProp = false; + + if (desc.isSome() && desc->isDataDescriptor()) { + intermediate = desc->value(); + hasProp = true; + } else { + // If we get here it means the object doesn't have the property or the + // property is available throuch a getter. We don't want to call any + // getters to avoid potential re-entrancy. + // The blob object is special since its properties are available + // only through getters but we still want to support them for key + // extraction. So they need to be handled manually. + Blob* blob; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { + if (token.EqualsLiteral("size")) { + ErrorResult rv; + uint64_t size = blob->GetSize(rv); + MOZ_ALWAYS_TRUE(!rv.Failed()); + + intermediate = JS_NumberValue(size); + hasProp = true; + } else if (token.EqualsLiteral("type")) { + nsString type; + blob->GetType(type); + + JSString* string = + JS_NewUCStringCopyN(aCx, type.get(), type.Length()); + + intermediate = JS::StringValue(string); + hasProp = true; + } else { + RefPtr<File> file = blob->ToFile(); + if (file) { + if (token.EqualsLiteral("name")) { + nsString name; + file->GetName(name); + + JSString* string = + JS_NewUCStringCopyN(aCx, name.get(), name.Length()); + + intermediate = JS::StringValue(string); + hasProp = true; + } else if (token.EqualsLiteral("lastModified")) { + ErrorResult rv; + int64_t lastModifiedDate = file->GetLastModified(rv); + MOZ_ALWAYS_TRUE(!rv.Failed()); + + intermediate = JS_NumberValue(lastModifiedDate); + hasProp = true; + } + // The spec also lists "lastModifiedDate", but we deprecated and + // removed support for it. + } + } + } + } + + if (hasProp) { + // 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; + } + QM_TRY(OkIf(succeeded.ok()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, + IDB_REPORT_INTERNAL_ERR_LAMBDA); + } + + // TODO: It would be nicer to do the cleanup using a RAII class or something. + // This last QM_TRY could be removed then. + QM_TRY(MOZ_TO_RESULT(rv)); + return NS_OK; +} + +} // namespace + +// static +Result<KeyPath, nsresult> KeyPath::Parse(const nsAString& aString) { + KeyPath keyPath(0); + keyPath.SetType(KeyPathType::String); + + if (!keyPath.AppendStringWithValidation(aString)) { + return Err(NS_ERROR_FAILURE); + } + + return keyPath; +} + +// static +Result<KeyPath, nsresult> KeyPath::Parse(const Sequence<nsString>& aStrings) { + KeyPath keyPath(0); + keyPath.SetType(KeyPathType::Array); + + for (uint32_t i = 0; i < aStrings.Length(); ++i) { + if (!keyPath.AppendStringWithValidation(aStrings[i])) { + return Err(NS_ERROR_FAILURE); + } + } + + return keyPath; +} + +// static +Result<KeyPath, nsresult> KeyPath::Parse( + const Nullable<OwningStringOrStringSequence>& aValue) { + if (aValue.IsNull()) { + return KeyPath{0}; + } + + if (aValue.Value().IsString()) { + return Parse(aValue.Value().GetAsString()); + } + + MOZ_ASSERT(aValue.Value().IsStringSequence()); + + const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence(); + if (seq.Length() == 0) { + return Err(NS_ERROR_FAILURE); + } + return Parse(seq); +} + +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; + } + + MOZ_ASSERT_UNREACHABLE("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; + } + + auto result = aKey.AppendItem(aCx, IsArray() && i == 0, value); + if (result.isErr()) { + NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset"); + if (result.inspectErr().Is(SpecialValues::Exception)) { + result.unwrapErr().AsException().SuppressException(); + } + 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; + } + + auto result = aKey.AppendItem(aCx, false, value); + if (result.isErr()) { + NS_ASSERTION(aKey.IsUnset(), "Should be unset"); + if (result.inspectErr().Is(SpecialValues::Exception)) { + result.unwrapErr().AsException().SuppressException(); + } + return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + + aKey.FinishArray(); + + return NS_OK; +} + +nsAutoString KeyPath::SerializeToString() const { + NS_ASSERTION(IsValid(), "Check to see if I'm valid first!"); + + if (IsString()) { + return nsAutoString{mStrings[0]}; + } + + if (IsArray()) { + nsAutoString res; + + // 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 :-) + const uint32_t len = mStrings.Length(); + for (uint32_t i = 0; i < len; ++i) { + res.Append(','); + res.Append(mStrings[i]); + } + + return res; + } + + MOZ_ASSERT_UNREACHABLE("What?"); + return {}; +} + +// static +KeyPath KeyPath::DeserializeFromString(const nsAString& aString) { + KeyPath keyPath(0); + + if (!aString.IsEmpty() && aString.First() == ',') { + keyPath.SetType(KeyPathType::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<NS_TokenizerIgnoreNothing> tokenizer( + aString, ','); + tokenizer.nextToken(); + while (tokenizer.hasMoreTokens()) { + keyPath.mStrings.AppendElement(tokenizer.nextToken()); + } + + if (tokenizer.separatorAfterCurrentToken()) { + // There is a trailing comma, indicating the original KeyPath has + // a trailing empty string, i.e. [..., '']. We should append this + // empty string. + keyPath.mStrings.EmplaceBack(); + } + + return keyPath; + } + + keyPath.SetType(KeyPathType::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 mozilla::dom::indexedDB diff --git a/dom/indexedDB/KeyPath.h b/dom/indexedDB/KeyPath.h new file mode 100644 index 0000000000..4e1042650f --- /dev/null +++ b/dom/indexedDB/KeyPath.h @@ -0,0 +1,120 @@ +/* -*- 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 <new> +#include <utility> +#include "js/TypeDecls.h" +#include "mozilla/Result.h" +#include "mozilla/ipc/IPCForwards.h" +#include "nsISupports.h" +#include "nsError.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace JS { +template <class T> +class Heap; +} + +namespace mozilla::dom { + +class OwningStringOrStringSequence; +template <typename T> +class Sequence; +template <typename T> +struct Nullable; + +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; + ALLOW_DEPRECATED_READPARAM + + KeyPath() : mType(KeyPathType::NonExistent) { MOZ_COUNT_CTOR(KeyPath); } + + public: + enum class KeyPathType { NonExistent, String, Array, EndGuard }; + + void SetType(KeyPathType aType); + + bool AppendStringWithValidation(const nsAString& aString); + + explicit KeyPath(int aDummy) : mType(KeyPathType::NonExistent) { + MOZ_COUNT_CTOR(KeyPath); + } + + KeyPath(KeyPath&& aOther) { + MOZ_COUNT_CTOR(KeyPath); + *this = std::move(aOther); + } + KeyPath& operator=(KeyPath&&) = default; + + KeyPath(const KeyPath& aOther) { + MOZ_COUNT_CTOR(KeyPath); + *this = aOther; + } + KeyPath& operator=(const KeyPath&) = default; + + MOZ_COUNTED_DTOR(KeyPath) + + static Result<KeyPath, nsresult> Parse(const nsAString& aString); + + static Result<KeyPath, nsresult> Parse(const Sequence<nsString>& aStrings); + + static Result<KeyPath, nsresult> Parse( + const Nullable<OwningStringOrStringSequence>& aValue); + + nsresult ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const; + + nsresult ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, + JS::Value* aOutVal) const; + + using ExtractOrCreateKeyCallback = nsresult (*)(JSContext*, void*); + + nsresult ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue, + Key& aKey, ExtractOrCreateKeyCallback aCallback, + void* aClosure) const; + + inline bool IsValid() const { return mType != KeyPathType::NonExistent; } + + inline bool IsArray() const { return mType == KeyPathType::Array; } + + inline bool IsString() const { return mType == KeyPathType::String; } + + inline bool IsEmpty() const { + return mType == KeyPathType::String && mStrings[0].IsEmpty(); + } + + bool operator==(const KeyPath& aOther) const { + return mType == aOther.mType && mStrings == aOther.mStrings; + } + + nsAutoString SerializeToString() 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; + + CopyableTArray<nsString> mStrings; +}; + +} // namespace indexedDB +} // namespace mozilla::dom + +#endif // mozilla_dom_indexeddb_keypath_h__ diff --git a/dom/indexedDB/PBackgroundIDBCursor.ipdl b/dom/indexedDB/PBackgroundIDBCursor.ipdl new file mode 100644 index 0000000000..60c8d2ca7b --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBCursor.ipdl @@ -0,0 +1,101 @@ +/* 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 PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; +include "mozilla/dom/indexedDB/ActorsChild.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.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; +}; + +// TODO: All cursor responses must be arrays! +union CursorResponse +{ + void_t; + nsresult; + ObjectStoreCursorResponse[]; + ObjectStoreKeyCursorResponse[]; + IndexCursorResponse[]; + IndexKeyCursorResponse[]; +}; + +[ChildImpl="indexedDB::BackgroundCursorChildBase", ParentImpl=virtual] +protocol PBackgroundIDBCursor +{ + manager PBackgroundIDBTransaction or PBackgroundIDBVersionChangeTransaction; + +parent: + async DeleteMe(); + + async Continue(CursorRequestParams params, Key currentKey, + Key currentObjectStoreKey); + +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 0000000000..82c0610a0b --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBDatabase.ipdl @@ -0,0 +1,63 @@ +/* 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 PBackgroundIDBFactory; +include protocol PBackgroundIDBTransaction; +include protocol PBackgroundIDBVersionChangeTransaction; + +include IPCBlob; +include InputStreamParams; +include PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; +include "mozilla/dom/indexedDB/ActorsChild.h"; + +using struct mozilla::null_t from "mozilla/ipc/IPCCore.h"; + +using mozilla::dom::IDBTransaction::Mode + from "mozilla/dom/IDBTransaction.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +[ManualDealloc, ChildImpl="indexedDB::BackgroundDatabaseChild", ParentImpl=virtual] +sync protocol PBackgroundIDBDatabase +{ + manager PBackgroundIDBFactory; + + manages PBackgroundIDBDatabaseFile; + manages PBackgroundIDBTransaction; + manages PBackgroundIDBVersionChangeTransaction; + +parent: + async DeleteMe(); + + async Blocked(); + + async Close(); + + async PBackgroundIDBDatabaseFile(IPCBlob blob); + + async PBackgroundIDBTransaction(nsString[] objectStoreNames, Mode mode); + +child: + async __delete__(); + + async VersionChange(uint64_t oldVersion, uint64_t? newVersion); + + async Invalidate(); + + async CloseAfterInvalidationComplete(); + + async PBackgroundIDBVersionChangeTransaction(uint64_t currentVersion, + uint64_t requestedVersion, + int64_t nextObjectStoreId, + int64_t nextIndexId); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBDatabaseFile.ipdl b/dom/indexedDB/PBackgroundIDBDatabaseFile.ipdl new file mode 100644 index 0000000000..4063e38c5a --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBDatabaseFile.ipdl @@ -0,0 +1,22 @@ +/* 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 { + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PBackgroundIDBDatabaseFile +{ + manager PBackgroundIDBDatabase; + +parent: + async __delete__(); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBFactory.ipdl b/dom/indexedDB/PBackgroundIDBFactory.ipdl new file mode 100644 index 0000000000..ef2a88e55b --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBFactory.ipdl @@ -0,0 +1,65 @@ +/* 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"; +include "mozilla/dom/indexedDB/ActorsChild.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.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; +}; + +[ChildImpl="indexedDB::BackgroundFactoryChild", ParentImpl=virtual] +sync protocol PBackgroundIDBFactory +{ + manager PBackground; + + manages PBackgroundIDBDatabase; + manages PBackgroundIDBFactoryRequest; + +parent: + async DeleteMe(); + + async PBackgroundIDBFactoryRequest(FactoryRequestParams params); + +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 0000000000..d85e7741b5 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBFactoryRequest.ipdl @@ -0,0 +1,46 @@ +/* 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; + +include "mozilla/dom/indexedDB/ActorsChild.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct OpenDatabaseRequestResponse +{ + PBackgroundIDBDatabase database; +}; + +struct DeleteDatabaseRequestResponse +{ + uint64_t previousVersion; +}; + +union FactoryRequestResponse +{ + nsresult; + OpenDatabaseRequestResponse; + DeleteDatabaseRequestResponse; +}; + +[ManualDealloc, ChildImpl="indexedDB::BackgroundFactoryRequestChild", ParentImpl=virtual] +protocol PBackgroundIDBFactoryRequest +{ + manager PBackgroundIDBFactory; + +child: + async __delete__(FactoryRequestResponse response); + + async Blocked(uint64_t currentVersion); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/PBackgroundIDBRequest.ipdl b/dom/indexedDB/PBackgroundIDBRequest.ipdl new file mode 100644 index 0000000000..293f65a598 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBRequest.ipdl @@ -0,0 +1,168 @@ +/* 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 PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; +include "mozilla/dom/indexedDB/ActorsChild.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.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 PreprocessInfo +{ + SerializedStructuredCloneFile[] files; +}; + +struct ObjectStoreGetPreprocessParams +{ + PreprocessInfo preprocessInfo; +}; + +struct ObjectStoreGetAllPreprocessParams +{ + PreprocessInfo[] 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; +}; + +[ManualDealloc, ChildImpl="indexedDB::BackgroundRequestChild", ParentImpl=virtual] +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 0000000000..3b05250e94 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBSharedTypes.ipdlh @@ -0,0 +1,297 @@ +/* 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 DOMTypes; +include IPCBlob; +include ProtocolTypes; + +include "mozilla/dom/indexedDB/SerializationHelpers.h"; +include "mozilla/dom/quota/SerializationHelpers.h"; + +using struct mozilla::null_t from "mozilla/ipc/IPCCore.h"; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; + +using mozilla::dom::IDBCursor::Direction + from "mozilla/dom/IDBCursor.h"; + +using mozilla::dom::indexedDB::StructuredCloneFileBase::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"; + +[MoveOnly=data] using mozilla::SerializedStructuredCloneBuffer + from "mozilla/ipc/SerializedStructuredCloneBuffer.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +struct SerializedKeyRange +{ + Key lower; + Key upper; + bool lowerOpen; + bool upperOpen; + bool isOnly; +}; + +union NullableBlob +{ + null_t; + IPCBlob; +}; + +struct SerializedStructuredCloneFile +{ + NullableBlob 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; +}; + +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 CommonOpenCursorParams +{ + int64_t objectStoreId; + SerializedKeyRange? optionalKeyRange; + Direction direction; +}; + +struct ObjectStoreOpenCursorParams +{ + CommonOpenCursorParams commonParams; +}; + +struct ObjectStoreOpenKeyCursorParams +{ + CommonOpenCursorParams commonParams; +}; + +struct CommonIndexOpenCursorParams +{ + CommonOpenCursorParams commonParams; + int64_t indexId; +}; + +struct IndexOpenCursorParams +{ + CommonIndexOpenCursorParams commonIndexParams; +}; + +struct IndexOpenKeyCursorParams +{ + CommonIndexOpenCursorParams commonIndexParams; +}; + +// TODO: Actually, using a union here is not very nice, unless IPDL supported +// struct inheritance. Alternatively, if IPDL supported enums, we could merge +// the subtypes into one. Using a plain integer for discriminating the +// subtypes would be too error-prone. +union OpenCursorParams +{ + ObjectStoreOpenCursorParams; + ObjectStoreOpenKeyCursorParams; + IndexOpenCursorParams; + IndexOpenKeyCursorParams; +}; + +struct FileAddInfo +{ + PBackgroundIDBDatabaseFile 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; + SerializedKeyRange? optionalKeyRange; + uint32_t limit; +}; + +struct ObjectStoreGetAllKeysParams +{ + int64_t objectStoreId; + SerializedKeyRange? optionalKeyRange; + uint32_t limit; +}; + +struct ObjectStoreDeleteParams +{ + int64_t objectStoreId; + SerializedKeyRange keyRange; +}; + +struct ObjectStoreClearParams +{ + int64_t objectStoreId; +}; + +struct ObjectStoreCountParams +{ + int64_t objectStoreId; + SerializedKeyRange? 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; + SerializedKeyRange? optionalKeyRange; + uint32_t limit; +}; + +struct IndexGetAllKeysParams +{ + int64_t objectStoreId; + int64_t indexId; + SerializedKeyRange? optionalKeyRange; + uint32_t limit; +}; + +struct IndexCountParams +{ + int64_t objectStoreId; + int64_t indexId; + SerializedKeyRange? 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 0000000000..85a9f76265 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBTransaction.ipdl @@ -0,0 +1,49 @@ +/* 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 PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/ActorsChild.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +[ChildImpl="indexedDB::BackgroundTransactionChild", ParentImpl=virtual] +protocol PBackgroundIDBTransaction { + manager PBackgroundIDBDatabase; + + manages PBackgroundIDBCursor; + manages PBackgroundIDBRequest; + +parent: + async DeleteMe(); + + // lastRequest is used with explicit commit to synchronize the + // transaction's committing state with the parent side, to abort the + // transaction in case of a request resulting in an error (see + // https://w3c.github.io/IndexedDB/#async-execute-request, step 5.3.). With + // automatic commit, this is not necessary, as the transaction's state will + // only be set to committing after the last request completed. + async Commit(int64_t? lastRequest); + 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 0000000000..e139f0f1c3 --- /dev/null +++ b/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl @@ -0,0 +1,51 @@ +/* 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 PBackgroundIDBSharedTypes; + +include "mozilla/dom/indexedDB/ActorsChild.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +[ChildImpl="indexedDB::BackgroundVersionChangeTransactionChild", ParentImpl=virtual] +protocol PBackgroundIDBVersionChangeTransaction { + manager PBackgroundIDBDatabase; + + manages PBackgroundIDBCursor; + manages PBackgroundIDBRequest; + +parent: + async DeleteMe(); + + async Commit(int64_t? lastRequest); + 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 0000000000..70214a483f --- /dev/null +++ b/dom/indexedDB/PBackgroundIndexedDBUtils.ipdl @@ -0,0 +1,38 @@ +/* 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"; +include "mozilla/dom/indexedDB/ActorsChild.h"; + +using mozilla::dom::quota::PersistenceType + from "mozilla/dom/quota/PersistenceType.h"; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +[ManualDealloc, ChildImpl="indexedDB::BackgroundUtilsChild", ParentImpl=virtual] +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, bool result); + +child: + async __delete__(); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla diff --git a/dom/indexedDB/ProfilerHelpers.cpp b/dom/indexedDB/ProfilerHelpers.cpp new file mode 100644 index 0000000000..2d5ebfc2e2 --- /dev/null +++ b/dom/indexedDB/ProfilerHelpers.cpp @@ -0,0 +1,281 @@ +/* -*- 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 "ProfilerHelpers.h" + +#include "BackgroundChildImpl.h" +#include "GeckoProfiler.h" +#include "IDBDatabase.h" +#include "IDBIndex.h" +#include "IDBKeyRange.h" +#include "IDBObjectStore.h" +#include "IDBTransaction.h" +#include "Key.h" +#include "ThreadLocal.h" + +#include "mozilla/dom/Event.h" +#include "nsReadableUtils.h" + +namespace mozilla::dom::indexedDB { + +namespace { +static const char kQuote = '\"'; +static const char kOpenBracket = '['; +static const char kCloseBracket = ']'; +static const char kOpenParen = '('; +static const char kCloseParen = ')'; + +void LoggingHelper(bool aUseProfiler, const char* aFmt, va_list args) { + 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_thread_is_being_profiled_for_markers())) { + nsAutoCString message; + + message.AppendVprintf(aFmt, args); + + MOZ_LOG(logModule, logLevel, ("%s", message.get())); + + if (aUseProfiler) { + PROFILER_MARKER_UNTYPED(message, DOM); + } + } +} +} // namespace + +template <bool CheckLoggingMode> +LoggingIdString<CheckLoggingMode>::LoggingIdString() { + using mozilla::ipc::BackgroundChildImpl; + + if (!CheckLoggingMode || IndexedDatabaseManager::GetLoggingMode() != + IndexedDatabaseManager::Logging_Disabled) { + const BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + if (threadLocal) { + const auto& idbThreadLocal = threadLocal->mIndexedDBThreadLocal; + if (idbThreadLocal) { + Assign(idbThreadLocal->IdString()); + } + } + } +} + +template <bool CheckLoggingMode> +LoggingIdString<CheckLoggingMode>::LoggingIdString(const nsID& aID) { + static_assert(NSID_LENGTH > 1, "NSID_LENGTH is set incorrectly!"); + static_assert(NSID_LENGTH <= kStorageSize, + "nsID string won't fit in our storage!"); + // Capacity() excludes the null terminator; NSID_LENGTH includes it. + MOZ_ASSERT(Capacity() + 1 == NSID_LENGTH); + + if (!CheckLoggingMode || 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())); + } +} + +template class LoggingIdString<false>; +template class LoggingIdString<true>; + +LoggingString::LoggingString(IDBDatabase* aDatabase) : nsAutoCString(kQuote) { + MOZ_ASSERT(aDatabase); + + AppendUTF16toUTF8(aDatabase->Name(), *this); + Append(kQuote); +} + +LoggingString::LoggingString(const IDBTransaction& aTransaction) + : nsAutoCString(kOpenBracket) { + constexpr auto kCommaSpace = ", "_ns; + + StringJoinAppend(*this, kCommaSpace, aTransaction.ObjectStoreNamesInternal(), + [](nsACString& dest, const auto& store) { + dest.Append(kQuote); + AppendUTF16toUTF8(store, dest); + dest.Append(kQuote); + }); + + Append(kCloseBracket); + Append(kCommaSpace); + + switch (aTransaction.GetMode()) { + case IDBTransaction::Mode::ReadOnly: + AppendLiteral("\"readonly\""); + break; + case IDBTransaction::Mode::ReadWrite: + AppendLiteral("\"readwrite\""); + break; + case IDBTransaction::Mode::ReadWriteFlush: + AppendLiteral("\"readwriteflush\""); + break; + case IDBTransaction::Mode::Cleanup: + AppendLiteral("\"cleanup\""); + break; + case IDBTransaction::Mode::VersionChange: + AppendLiteral("\"versionchange\""); + break; + default: + MOZ_CRASH("Unknown mode!"); + }; +} + +LoggingString::LoggingString(IDBObjectStore* aObjectStore) + : nsAutoCString(kQuote) { + MOZ_ASSERT(aObjectStore); + + AppendUTF16toUTF8(aObjectStore->Name(), *this); + Append(kQuote); +} + +LoggingString::LoggingString(IDBIndex* aIndex) : nsAutoCString(kQuote) { + MOZ_ASSERT(aIndex); + + AppendUTF16toUTF8(aIndex->Name(), *this); + Append(kQuote); +} + +LoggingString::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>"); + } +} + +LoggingString::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()) { + AppendPrintf("\"%s\"", NS_ConvertUTF16toUTF8(aKey.ToString()).get()); + } else if (aKey.IsBinary()) { + AssignLiteral("[object ArrayBuffer]"); + } else { + MOZ_ASSERT(aKey.IsArray()); + AssignLiteral("[...]"); + } +} + +LoggingString::LoggingString(const IDBCursorDirection aDirection) { + switch (aDirection) { + case IDBCursorDirection::Next: + AssignLiteral("\"next\""); + break; + case IDBCursorDirection::Nextunique: + AssignLiteral("\"nextunique\""); + break; + case IDBCursorDirection::Prev: + AssignLiteral("\"prev\""); + break; + case IDBCursorDirection::Prevunique: + AssignLiteral("\"prevunique\""); + break; + default: + MOZ_CRASH("Unknown direction!"); + }; +} + +LoggingString::LoggingString(const Optional<uint64_t>& aVersion) { + if (aVersion.WasPassed()) { + AppendInt(aVersion.Value()); + } else { + AssignLiteral("<undefined>"); + } +} + +LoggingString::LoggingString(const Optional<uint32_t>& aLimit) { + if (aLimit.WasPassed()) { + AppendInt(aLimit.Value()); + } else { + AssignLiteral("<undefined>"); + } +} + +LoggingString::LoggingString(IDBObjectStore* aObjectStore, const Key& aKey) { + MOZ_ASSERT(aObjectStore); + + if (!aObjectStore->HasValidKeyPath()) { + Append(LoggingString(aKey)); + } +} + +LoggingString::LoggingString(Event* aEvent, const char16_t* aDefault) + : nsAutoCString(kQuote) { + MOZ_ASSERT(aDefault); + + nsAutoString eventType; + + if (aEvent) { + aEvent->GetType(eventType); + } else { + eventType = nsDependentString(aDefault); + } + + AppendUTF16toUTF8(eventType, *this); + Append(kQuote); +} + +void LoggingHelper(const char* aDetailedFmt, const char* aConciseFmt, ...) { + const IndexedDatabaseManager::LoggingMode mode = + IndexedDatabaseManager::GetLoggingMode(); + + if (mode != IndexedDatabaseManager::Logging_Disabled) { + const char* fmt; + if (mode == IndexedDatabaseManager::Logging_Concise || + mode == IndexedDatabaseManager::Logging_ConciseProfilerMarks) { + fmt = aConciseFmt; + } else { + MOZ_ASSERT(mode == IndexedDatabaseManager::Logging_Detailed || + mode == IndexedDatabaseManager::Logging_DetailedProfilerMarks); + fmt = aDetailedFmt; + } + + const bool useProfiler = + mode == IndexedDatabaseManager::Logging_ConciseProfilerMarks || + mode == IndexedDatabaseManager::Logging_DetailedProfilerMarks; + + va_list args; + va_start(args, aConciseFmt); + + LoggingHelper(useProfiler, fmt, args); + + va_end(args); + } +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/ProfilerHelpers.h b/dom/indexedDB/ProfilerHelpers.h new file mode 100644 index 0000000000..efba9d9cd0 --- /dev/null +++ b/dom/indexedDB/ProfilerHelpers.h @@ -0,0 +1,145 @@ +/* -*- 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 "IndexedDatabaseManager.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/IDBCursorBinding.h" +#include "nsID.h" +#include "nsString.h" + +namespace mozilla::dom { + +class Event; +class IDBDatabase; +class IDBIndex; +class IDBKeyRange; +class IDBObjectStore; +class IDBTransaction; + +namespace indexedDB { + +class Key; + +template <bool CheckLoggingMode> +class LoggingIdString final : public nsAutoCStringN<NSID_LENGTH> { + public: + LoggingIdString(); + + explicit LoggingIdString(const nsID& aID); +}; + +class MOZ_STACK_CLASS LoggingString final : public nsAutoCString { + public: + explicit LoggingString(IDBDatabase* aDatabase); + explicit LoggingString(const IDBTransaction& aTransaction); + explicit LoggingString(IDBObjectStore* aObjectStore); + explicit LoggingString(IDBIndex* aIndex); + explicit LoggingString(IDBKeyRange* aKeyRange); + explicit LoggingString(const Key& aKey); + explicit LoggingString(const IDBCursorDirection aDirection); + explicit LoggingString(const Optional<uint64_t>& aVersion); + explicit LoggingString(const Optional<uint32_t>& aLimit); + + LoggingString(IDBObjectStore* aObjectStore, const Key& aKey); + LoggingString(Event* aEvent, const char16_t* aDefault); +}; + +// Both the aDetailedFmt and the aConciseFmt need to match the variable argument +// list, so we use MOZ_FORMAT_PRINTF twice here. +void MOZ_FORMAT_PRINTF(1, 3) MOZ_FORMAT_PRINTF(2, 3) + LoggingHelper(const char* aDetailedFmt, const char* aConciseFmt, ...); + +} // namespace indexedDB +} // namespace mozilla::dom + +#define IDB_LOG_MARK(_detailedFmt, _conciseFmt, _loggingId, ...) \ + mozilla::dom::indexedDB::LoggingHelper("IndexedDB %s: " _detailedFmt, \ + "IndexedDB %s: " _conciseFmt, \ + _loggingId, ##__VA_ARGS__) + +#define IDB_LOG_ID_STRING(...) \ + mozilla::dom::indexedDB::LoggingIdString<true>(__VA_ARGS__).get() + +#define IDB_LOG_STRINGIFY(...) \ + mozilla::dom::indexedDB::LoggingString(__VA_ARGS__).get() + +// IDB_LOG_MARK_DETAILED_PARENT and IDB_LOG_MARK_DETAILED_CHILD should have the +// same width. +#define IDB_LOG_MARK_DETAILED_PARENT "Parent" +#define IDB_LOG_MARK_DETAILED_CHILD "Child " + +#define IDB_LOG_MARK_CONCISE_PARENT "P" +#define IDB_LOG_MARK_CONCISE_CHILD "C" + +#define IDB_LOG_MARK_DETAILED_TRANSACTION "Transaction[%" PRIi64 "]" +#define IDB_LOG_MARK_DETAILED_REQUEST "Request[%" PRIu64 "]" + +#define IDB_LOG_MARK_CONCISE_TRANSACTION "T[%" PRIi64 "]" +#define IDB_LOG_MARK_CONCISE_REQUEST "R[%" PRIu64 "]" + +#define IDB_LOG_MARK_TRANSACTION_REQUEST( \ + _detailedPeer, _concisePeer, _detailedFmt, _conciseFmt, _loggingId, \ + _transactionSerialNumber, _requestSerialNumber, ...) \ + IDB_LOG_MARK(_detailedPeer " " IDB_LOG_MARK_DETAILED_TRANSACTION \ + " " IDB_LOG_MARK_DETAILED_REQUEST \ + ": " _detailedFmt, \ + _concisePeer " " IDB_LOG_MARK_CONCISE_TRANSACTION \ + " " IDB_LOG_MARK_CONCISE_REQUEST ": " _conciseFmt, \ + _loggingId, _transactionSerialNumber, _requestSerialNumber, \ + ##__VA_ARGS__) + +#define IDB_LOG_MARK_PARENT_TRANSACTION_REQUEST( \ + _detailedFmt, _conciseFmt, _loggingId, _transactionSerialNumber, \ + _requestSerialNumber, ...) \ + IDB_LOG_MARK_TRANSACTION_REQUEST( \ + IDB_LOG_MARK_DETAILED_PARENT, IDB_LOG_MARK_CONCISE_PARENT, _detailedFmt, \ + _conciseFmt, _loggingId, _transactionSerialNumber, _requestSerialNumber, \ + ##__VA_ARGS__) + +#define IDB_LOG_MARK_CHILD_TRANSACTION_REQUEST(_detailedFmt, _conciseFmt, \ + _transactionSerialNumber, \ + _requestSerialNumber, ...) \ + IDB_LOG_MARK_TRANSACTION_REQUEST( \ + IDB_LOG_MARK_DETAILED_CHILD, IDB_LOG_MARK_CONCISE_CHILD, _detailedFmt, \ + _conciseFmt, IDB_LOG_ID_STRING(), _transactionSerialNumber, \ + _requestSerialNumber, ##__VA_ARGS__) + +#define IDB_LOG_MARK_CHILD_REQUEST(_detailedFmt, _conciseFmt, \ + _requestSerialNumber, ...) \ + IDB_LOG_MARK(IDB_LOG_MARK_DETAILED_CHILD " " IDB_LOG_MARK_DETAILED_REQUEST \ + ": " _detailedFmt, \ + IDB_LOG_MARK_CONCISE_CHILD " " IDB_LOG_MARK_CONCISE_REQUEST \ + ": " _conciseFmt, \ + IDB_LOG_ID_STRING(), _requestSerialNumber, ##__VA_ARGS__) + +#define IDB_LOG_MARK_TRANSACTION(_detailedPeer, _concisePeer, _detailedFmt, \ + _conciseFmt, _loggingId, \ + _transactionSerialNumber, ...) \ + IDB_LOG_MARK( \ + _detailedPeer " " IDB_LOG_MARK_DETAILED_TRANSACTION ": " _detailedFmt, \ + _concisePeer " " IDB_LOG_MARK_CONCISE_TRANSACTION ": " _conciseFmt, \ + _loggingId, _transactionSerialNumber, ##__VA_ARGS__) + +#define IDB_LOG_MARK_PARENT_TRANSACTION(_detailedFmt, _conciseFmt, _loggingId, \ + _transactionSerialNumber, ...) \ + IDB_LOG_MARK_TRANSACTION( \ + IDB_LOG_MARK_DETAILED_PARENT, IDB_LOG_MARK_CONCISE_PARENT, _detailedFmt, \ + _conciseFmt, _loggingId, _transactionSerialNumber, ##__VA_ARGS__) + +#define IDB_LOG_MARK_CHILD_TRANSACTION(_detailedFmt, _conciseFmt, \ + _transactionSerialNumber, ...) \ + IDB_LOG_MARK_TRANSACTION(IDB_LOG_MARK_DETAILED_CHILD, \ + IDB_LOG_MARK_CONCISE_CHILD, _detailedFmt, \ + _conciseFmt, IDB_LOG_ID_STRING(), \ + _transactionSerialNumber, ##__VA_ARGS__) + +#endif // mozilla_dom_indexeddb_profilerhelpers_h__ diff --git a/dom/indexedDB/ReportInternalError.cpp b/dom/indexedDB/ReportInternalError.cpp new file mode 100644 index 0000000000..fb750c258c --- /dev/null +++ b/dom/indexedDB/ReportInternalError.cpp @@ -0,0 +1,31 @@ +/* -*- 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::dom::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:%" PRIu32, aStr, aFile, aLine)), + "indexedDB"_ns, false /* no IDB in private window */, + true /* Internal errors are chrome context only */); +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/ReportInternalError.h b/dom/indexedDB/ReportInternalError.h new file mode 100644 index 0000000000..08585e5258 --- /dev/null +++ b/dom/indexedDB/ReportInternalError.h @@ -0,0 +1,35 @@ +/* -*- 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 "nsPrintfCString.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") + +#define IDB_REPORT_INTERNAL_ERR_LAMBDA \ + [](const auto&) { IDB_REPORT_INTERNAL_ERR(); } + +namespace mozilla::dom::indexedDB { + +MOZ_COLD void ReportInternalError(const char* aFile, uint32_t aLine, + const char* aStr); + +} // namespace mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_reportinternalerror_h__ diff --git a/dom/indexedDB/SafeRefPtr.h b/dom/indexedDB/SafeRefPtr.h new file mode 100644 index 0000000000..3d7a5d635e --- /dev/null +++ b/dom/indexedDB/SafeRefPtr.h @@ -0,0 +1,481 @@ +/* -*- 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_saferefptr_h__ +#define mozilla_saferefptr_h__ + +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/Maybe.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefCounted.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsTObserverArray.h" + +namespace mozilla { +template <typename T> +class SafeRefPtr; + +template <typename T, typename... Args> +SafeRefPtr<T> MakeSafeRefPtr(Args&&... aArgs); + +namespace detail { +struct InitialConstructionTag {}; + +class SafeRefCountedBase { + template <typename U, typename... Args> + friend SafeRefPtr<U> mozilla::MakeSafeRefPtr(Args&&... aArgs); + + template <typename T> + friend class SafeRefPtr; + + void* operator new(size_t aSize) { return ::operator new(aSize); } + + protected: + void operator delete(void* aPtr) { ::operator delete(aPtr); } + + public: + void* operator new[](size_t) = delete; +}; + +// SafeRefCounted is similar to RefCounted, but they differ in their initial +// refcount (here 1), and the visibility of operator new (here private). The +// rest is mostly a copy of RefCounted. +template <typename T, RefCountAtomicity Atomicity> +class SafeRefCounted : public SafeRefCountedBase { + protected: + SafeRefCounted() = default; +#ifdef DEBUG + ~SafeRefCounted() { MOZ_ASSERT(mRefCnt == detail::DEAD); } +#endif + + public: + // Compatibility with nsRefPtr. + MozRefCountType AddRef() const { + // Note: this method must be thread safe for AtomicRefCounted. + MOZ_ASSERT(int32_t(mRefCnt) >= 0); + const MozRefCountType cnt = ++mRefCnt; + detail::RefCountLogger::logAddRef(static_cast<const T*>(this), cnt); + return cnt; + } + + MozRefCountType Release() const { + // Note: this method must be thread safe for AtomicRefCounted. + MOZ_ASSERT(int32_t(mRefCnt) > 0); + detail::RefCountLogger::ReleaseLogger logger(static_cast<const T*>(this)); + const MozRefCountType cnt = --mRefCnt; + // Note: it's not safe to touch |this| after decrementing the refcount, + // except for below. + logger.logRelease(cnt); + if (0 == cnt) { + // Because we have atomically decremented the refcount above, only + // one thread can get a 0 count here, so as long as we can assume that + // everything else in the system is accessing this object through + // RefPtrs, it's safe to access |this| here. +#ifdef DEBUG + mRefCnt = detail::DEAD; +#endif + delete static_cast<const T*>(this); + } + return cnt; + } + + // Compatibility with wtf::RefPtr. + void ref() { AddRef(); } + void deref() { Release(); } + MozRefCountType refCount() const { return mRefCnt; } + bool hasOneRef() const { + MOZ_ASSERT(mRefCnt > 0); + return mRefCnt == 1; + } + + protected: + SafeRefPtr<T> SafeRefPtrFromThis(); + + private: + mutable RC<MozRefCountType, Atomicity> mRefCnt = + RC<MozRefCountType, Atomicity>{1}; +}; +} // namespace detail + +template <typename T> +class SafeRefCounted + : public detail::SafeRefCounted<T, detail::NonAtomicRefCount> { + public: + ~SafeRefCounted() { + static_assert(std::is_base_of<SafeRefCounted, T>::value, + "T must derive from SafeRefCounted<T>"); + } +}; + +template <typename T> +class AtomicSafeRefCounted + : public detail::SafeRefCounted<T, detail::AtomicRefCount> { + public: + ~AtomicSafeRefCounted() { + static_assert(std::is_base_of<AtomicSafeRefCounted, T>::value, + "T must derive from AtomicSafeRefCounted<T>"); + } +}; + +struct AcquireStrongRefFromRawPtr {}; + +// XXX for Apple, clang::trivial_abi is probably also supported, but we need to +// find out the correct version number +#if defined(__clang__) && !defined(__apple_build_version__) && \ + __clang_major__ >= 7 +# define MOZ_TRIVIAL_ABI [[clang::trivial_abi]] +#else +# define MOZ_TRIVIAL_ABI +#endif + +// A restricted variant of mozilla::RefPtr<T>, which prohibits some unsafe or +// unperformant misuses, in particular: +// * It is not implicitly convertible from a raw pointer. Unsafe acquisitions +// from a raw pointer must be made using the verbose +// AcquireStrongRefFromRawPtr. To create a new object on the heap, use +// MakeSafeRefPtr. +// * It does not implicitly decay to a raw pointer. unsafeGetRawPtr() must be +// called +// explicitly. +// * It is not copyable, but must be explicitly copied using clonePtr(). +// * Temporaries cannot be dereferenced using operator* or operator->. +template <typename T> +class MOZ_IS_REFPTR MOZ_TRIVIAL_ABI SafeRefPtr { + template <typename U> + friend class SafeRefPtr; + + template <typename U, typename... Args> + friend SafeRefPtr<U> mozilla::MakeSafeRefPtr(Args&&... aArgs); + + T* MOZ_OWNING_REF mRawPtr = nullptr; + + // BEGIN Some things copied from RefPtr. + // We cannot simply use a RefPtr member because we want to be trivial_abi, + // which RefPtr is not. + void assign_with_AddRef(T* aRawPtr) { + if (aRawPtr) { + ConstRemovingRefPtrTraits<T>::AddRef(aRawPtr); + } + assign_assuming_AddRef(aRawPtr); + } + + void assign_assuming_AddRef(T* aNewPtr) { + T* oldPtr = mRawPtr; + mRawPtr = aNewPtr; + if (oldPtr) { + ConstRemovingRefPtrTraits<T>::Release(oldPtr); + } + } + + template <class U> + struct ConstRemovingRefPtrTraits { + static void AddRef(U* aPtr) { mozilla::RefPtrTraits<U>::AddRef(aPtr); } + static void Release(U* aPtr) { mozilla::RefPtrTraits<U>::Release(aPtr); } + }; + template <class U> + struct ConstRemovingRefPtrTraits<const U> { + static void AddRef(const U* aPtr) { + mozilla::RefPtrTraits<U>::AddRef(const_cast<U*>(aPtr)); + } + static void Release(const U* aPtr) { + mozilla::RefPtrTraits<U>::Release(const_cast<U*>(aPtr)); + } + }; + // END Some things copied from RefPtr. + + SafeRefPtr(T* aRawPtr, mozilla::detail::InitialConstructionTag); + + public: + SafeRefPtr() = default; + + template <typename U, + typename = std::enable_if_t<std::is_convertible_v<U*, T*>>> + MOZ_IMPLICIT SafeRefPtr(SafeRefPtr<U>&& aSrc) : mRawPtr(aSrc.mRawPtr) { + aSrc.mRawPtr = nullptr; + } + + explicit SafeRefPtr(RefPtr<T>&& aRefPtr) : mRawPtr(aRefPtr.forget().take()) {} + + // To prevent implicit conversion of raw pointer to RefPtr and then + // calling the previous overload. + SafeRefPtr(T* const aRawPtr) = delete; + + SafeRefPtr(T* const aRawPtr, const AcquireStrongRefFromRawPtr&) { + assign_with_AddRef(aRawPtr); + } + + MOZ_IMPLICIT SafeRefPtr(std::nullptr_t) {} + + // Prevent implicit copying, use clonePtr() instead. + SafeRefPtr(const SafeRefPtr&) = delete; + SafeRefPtr& operator=(const SafeRefPtr&) = delete; + + // Allow moving. + SafeRefPtr(SafeRefPtr&& aOther) noexcept : mRawPtr(aOther.mRawPtr) { + aOther.mRawPtr = nullptr; + } + SafeRefPtr& operator=(SafeRefPtr&& aOther) noexcept { + assign_assuming_AddRef(aOther.mRawPtr); + aOther.mRawPtr = nullptr; + return *this; + } + + ~SafeRefPtr() { + static_assert(!std::is_copy_constructible_v<T>); + static_assert(!std::is_copy_assignable_v<T>); + static_assert(!std::is_move_constructible_v<T>); + static_assert(!std::is_move_assignable_v<T>); + + if (mRawPtr) { + ConstRemovingRefPtrTraits<T>::Release(mRawPtr); + } + } + + typedef T element_type; + + explicit operator bool() const { return mRawPtr; } + bool operator!() const { return !mRawPtr; } + + T& operator*() const&& = delete; + + T& operator*() const& { + MOZ_ASSERT(mRawPtr); + return *mRawPtr; + } + + T* operator->() const&& = delete; + + T* operator->() const& MOZ_NO_ADDREF_RELEASE_ON_RETURN { + MOZ_ASSERT(mRawPtr); + return mRawPtr; + } + + Maybe<T&> maybeDeref() const { return ToMaybeRef(mRawPtr); } + + T* unsafeGetRawPtr() const { return mRawPtr; } + + SafeRefPtr<T> clonePtr() const { + return SafeRefPtr{mRawPtr, AcquireStrongRefFromRawPtr{}}; + } + + already_AddRefed<T> forget() { + auto* const res = mRawPtr; + mRawPtr = nullptr; + return dont_AddRef(res); + } + + bool operator==(const SafeRefPtr<T>& aOther) const { + return mRawPtr == aOther.mRawPtr; + } + + bool operator!=(const SafeRefPtr<T>& aOther) const { + return mRawPtr != aOther.mRawPtr; + } + + template <typename U, typename = std::enable_if_t<std::is_base_of_v<T, U>>> + SafeRefPtr<U> downcast() && { + SafeRefPtr<U> res; + res.mRawPtr = static_cast<U*>(mRawPtr); + mRawPtr = nullptr; + return res; + } + + template <typename U> + friend RefPtr<U> AsRefPtr(SafeRefPtr<U>&& aSafeRefPtr); +}; + +template <typename T> +SafeRefPtr(RefPtr<T>&&) -> SafeRefPtr<T>; + +template <typename T> +SafeRefPtr(already_AddRefed<T>&&) -> SafeRefPtr<T>; + +template <typename T> +class CheckedUnsafePtr; + +template <typename T> +SafeRefPtr(const CheckedUnsafePtr<T>&, const AcquireStrongRefFromRawPtr&) + -> SafeRefPtr<T>; + +template <typename T> +SafeRefPtr<T>::SafeRefPtr(T* aRawPtr, detail::InitialConstructionTag) + : mRawPtr(aRawPtr) { + if (!std::is_base_of_v<detail::SafeRefCountedBase, T> && mRawPtr) { + ConstRemovingRefPtrTraits<T>::AddRef(mRawPtr); + } +} + +template <typename T> +bool operator==(std::nullptr_t aLhs, const SafeRefPtr<T>& aRhs) { + return !aRhs; +} + +template <typename T> +bool operator!=(std::nullptr_t aLhs, const SafeRefPtr<T>& aRhs) { + return static_cast<bool>(aRhs); +} + +template <typename T> +bool operator==(const SafeRefPtr<T>& aLhs, std::nullptr_t aRhs) { + return !aLhs; +} + +template <typename T> +bool operator!=(const SafeRefPtr<T>& aLhs, std::nullptr_t aRhs) { + return static_cast<bool>(aLhs); +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator==(T* const aLhs, const SafeRefPtr<U>& aRhs) { + return aLhs == aRhs.unsafeGetRawPtr(); +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator!=(T* const aLhs, const SafeRefPtr<U>& aRhs) { + return !(aLhs == aRhs); +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator==(const SafeRefPtr<T>& aLhs, U* const aRhs) { + return aRhs == aLhs; +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator!=(const SafeRefPtr<T>& aLhs, U* const aRhs) { + return aRhs != aLhs; +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator==(const Maybe<T&> aLhs, const SafeRefPtr<U>& aRhs) { + return &aLhs.ref() == aRhs.unsafeGetRawPtr(); +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator!=(const Maybe<T&> aLhs, const SafeRefPtr<U>& aRhs) { + return !(aLhs == aRhs); +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator==(const SafeRefPtr<T>& aLhs, const Maybe<U&> aRhs) { + return aRhs == aLhs; +} + +template <typename T, typename U, typename = std::common_type_t<T*, U*>> +bool operator!=(const SafeRefPtr<T>& aLhs, const Maybe<U&> aRhs) { + return aRhs != aLhs; +} + +template <typename T> +RefPtr<T> AsRefPtr(SafeRefPtr<T>&& aSafeRefPtr) { + return aSafeRefPtr.forget(); +} + +template <typename T, typename... Args> +SafeRefPtr<T> MakeSafeRefPtr(Args&&... aArgs) { + return SafeRefPtr{new T(std::forward<Args>(aArgs)...), + detail::InitialConstructionTag{}}; +} + +template <typename T> +void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + const SafeRefPtr<T>& aField, const char* aName, + uint32_t aFlags = 0) { + CycleCollectionNoteChild(aCallback, aField.unsafeGetRawPtr(), aName, aFlags); +} + +template <typename T> +void ImplCycleCollectionUnlink(SafeRefPtr<T>& aField) { + aField = nullptr; +} + +namespace detail { + +template <typename T, RefCountAtomicity Atomicity> +SafeRefPtr<T> SafeRefCounted<T, Atomicity>::SafeRefPtrFromThis() { + // this actually is safe + return {static_cast<T*>(this), AcquireStrongRefFromRawPtr{}}; +} + +template <typename T> +struct CopyablePtr<SafeRefPtr<T>> { + SafeRefPtr<T> mPtr; + + explicit CopyablePtr(SafeRefPtr<T> aPtr) : mPtr{std::move(aPtr)} {} + + CopyablePtr(const CopyablePtr& aOther) : mPtr{aOther.mPtr.clonePtr()} {} + CopyablePtr& operator=(const CopyablePtr& aOther) { + if (this != &aOther) { + mPtr = aOther.mPtr.clonePtr(); + } + return *this; + } + CopyablePtr(CopyablePtr&&) = default; + CopyablePtr& operator=(CopyablePtr&&) = default; +}; + +} // namespace detail + +namespace dom { +/// XXX Move this to BindingUtils.h later on +template <class T, class S> +inline RefPtr<T> StrongOrRawPtr(SafeRefPtr<S>&& aPtr) { + return AsRefPtr(std::move(aPtr)); +} + +} // namespace dom + +} // namespace mozilla + +template <class T> +class nsTObserverArray<mozilla::SafeRefPtr<T>> + : public nsAutoTObserverArray<mozilla::SafeRefPtr<T>, 0> { + public: + using base_type = nsAutoTObserverArray<mozilla::SafeRefPtr<T>, 0>; + using size_type = nsTObserverArray_base::size_type; + + // Initialization methods + nsTObserverArray() = default; + + // Initialize this array and pre-allocate some number of elements. + explicit nsTObserverArray(size_type aCapacity) { + base_type::mArray.SetCapacity(aCapacity); + } + + nsTObserverArray Clone() const { + auto result = nsTObserverArray{}; + result.mArray = mozilla::TransformIntoNewArray( + this->mArray, [](const auto& ptr) { return ptr.clonePtr(); }); + return result; + } +}; + +// Use MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED in a 'Class' derived from a +// 'Super' class which derives from (Atomic)SafeRefCounted, and from some other +// class using NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING. +#if defined(NS_BUILD_REFCNT_LOGGING) +# define MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(Class, Super) \ + template <typename T, ::mozilla::detail::RefCountAtomicity Atomicity> \ + friend class ::mozilla::detail::SafeRefCounted; \ + NS_IMETHOD_(MozExternalRefCountType) AddRef() override { \ + NS_IMPL_ADDREF_INHERITED_GUTS(Class, Super); \ + } \ + NS_IMETHOD_(MozExternalRefCountType) Release() override { \ + NS_IMPL_RELEASE_INHERITED_GUTS(Class, Super); \ + } +#else // NS_BUILD_REFCNT_LOGGING +# define MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(Class, Super) \ + template <typename T, ::mozilla::detail::RefCountAtomicity Atomicity> \ + friend class ::mozilla::detail::SafeRefCounted; \ + NS_IMETHOD_(MozExternalRefCountType) AddRef() override { \ + return Super::AddRef(); \ + } \ + NS_IMETHOD_(MozExternalRefCountType) Release() override { \ + return Super::Release(); \ + } +#endif + +#endif diff --git a/dom/indexedDB/SchemaUpgrades.cpp b/dom/indexedDB/SchemaUpgrades.cpp new file mode 100644 index 0000000000..d33ebafd6a --- /dev/null +++ b/dom/indexedDB/SchemaUpgrades.cpp @@ -0,0 +1,3022 @@ +/* -*- 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 "SchemaUpgrades.h" + +// local includes +#include "ActorsParentCommon.h" +#include "DatabaseFileInfo.h" +#include "DatabaseFileManager.h" +#include "DBSchema.h" +#include "IndexedDatabase.h" +#include "IndexedDatabaseInlines.h" +#include "IndexedDBCommon.h" +#include "ReportInternalError.h" + +// global includes +#include <stdlib.h> +#include <algorithm> +#include <tuple> +#include <type_traits> +#include <utility> +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "SafeRefPtr.h" +#include "js/RootingAPI.h" +#include "js/StructuredClone.h" +#include "js/Value.h" +#include "jsapi.h" +#include "mozIStorageConnection.h" +#include "mozIStorageFunction.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "mozStorageHelper.h" +#include "mozilla/Assertions.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Monitor.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Span.h" +#include "mozilla/TaskCategory.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/indexedDB/IDBResult.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/fallible.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/mozalloc.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/storage/Variant.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsISupports.h" +#include "nsIVariant.h" +#include "nsLiteralString.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTLiteralString.h" +#include "nsTStringRepr.h" +#include "nsThreadUtils.h" +#include "nscore.h" +#include "snappy/snappy.h" + +struct JSContext; +class JSObject; + +#if defined(MOZ_WIDGET_ANDROID) +# define IDB_MOBILE +#endif + +namespace mozilla::dom::indexedDB { + +using mozilla::ipc::IsOnBackgroundThread; +using quota::AssertIsOnIOThread; +using quota::PERSISTENCE_TYPE_INVALID; + +namespace { + +nsresult UpgradeSchemaFrom4To5(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom4To5", DOM); + + 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( + "SELECT name, version, dataVersion " + "FROM database"_ns, + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsString name; + int32_t intVersion; + int64_t dataVersion; + + { + mozStorageStatementScoper scoper(stmt); + + QM_TRY_INSPECT(const bool& hasResults, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, ExecuteStep)); + + 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("DROP TABLE database"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE TABLE database (" + "name TEXT NOT NULL, " + "version INTEGER NOT NULL DEFAULT 0, " + "dataVersion INTEGER NOT NULL" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + rv = aConnection.CreateStatement( + "INSERT INTO database (name, version, dataVersion) " + "VALUES (:name, :version, :dataVersion)"_ns, + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + { + mozStorageStatementScoper scoper(stmt); + + rv = stmt->BindStringByIndex(0, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByIndex(1, intVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt64ByIndex(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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom5To6", DOM); + + // First, drop all the indexes we're no longer going to use. + nsresult rv = aConnection.ExecuteSimpleSQL("DROP INDEX key_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP INDEX ai_key_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP INDEX value_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP INDEX ai_value_index;"_ns); + 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( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "key_value, " + "data " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, key_value, data " + "FROM object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_data " + "SELECT id, object_store_id, key_value, data " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + 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( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "data " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, data " + "FROM ai_object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO ai_object_data " + "SELECT id, object_store_id, data " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + 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( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT OR IGNORE INTO index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX index_data_object_data_id_index " + "ON index_data (object_data_id);"_ns); + 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( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO unique_index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX unique_index_data_object_data_id_index " + "ON unique_index_data (object_data_id);"_ns); + 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( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "ai_object_data_id " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, ai_object_data_id " + "FROM ai_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT OR IGNORE INTO ai_index_data " + "SELECT index_id, value, ai_object_data_id " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX ai_index_data_ai_object_data_id_index " + "ON ai_index_data (ai_object_data_id);"_ns); + 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( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "ai_object_data_id " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT index_id, value, ai_object_data_id " + "FROM ai_unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO ai_unique_index_data " + "SELECT index_id, value, ai_object_data_id " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX ai_unique_index_data_ai_object_data_id_index " + "ON ai_unique_index_data (ai_object_data_id);"_ns); + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom6To7", DOM); + + nsresult rv = aConnection.ExecuteSimpleSQL( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id, " + "name, " + "key_path, " + "auto_increment" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT id, name, key_path, auto_increment " + "FROM object_store;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE TABLE object_store (" + "id INTEGER PRIMARY KEY, " + "auto_increment INTEGER NOT NULL DEFAULT 0, " + "name TEXT NOT NULL, " + "key_path TEXT, " + "UNIQUE (name)" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_store " + "SELECT id, auto_increment, name, nullif(key_path, '') " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom7To8", DOM); + + nsresult rv = aConnection.ExecuteSimpleSQL( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id, " + "object_store_id, " + "name, " + "key_path, " + "unique_index, " + "object_store_autoincrement" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, name, key_path, " + "unique_index, object_store_autoincrement " + "FROM object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_store_index " + "SELECT id, object_store_id, name, key_path, " + "unique_index, 0, object_store_autoincrement " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + 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() = default; + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override { + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + AUTO_PROFILER_LABEL("CompressDataBlobsFunction::OnFunctionCall", DOM); + + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom8To9_0", DOM); + + // We no longer use the dataVersion column. + nsresult rv = + aConnection.ExecuteSimpleSQL("UPDATE database SET dataVersion = 0;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIStorageFunction> compressor = new CompressDataBlobsFunction(); + + constexpr auto compressorName = "compress"_ns; + + 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( + "UPDATE object_data SET data = compress(data);"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "UPDATE ai_object_data SET data = compress(data);"_ns); + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom9_0To10_0", DOM); + + nsresult rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE object_data ADD COLUMN file_ids TEXT;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE ai_object_data ADD COLUMN file_ids TEXT;"_ns); + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom10_0To11_0", DOM); + + nsresult rv = aConnection.ExecuteSimpleSQL( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id, " + "object_store_id, " + "name, " + "key_path, " + "unique_index, " + "multientry" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, name, key_path, " + "unique_index, multientry " + "FROM object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_store_index " + "SELECT id, object_store_id, name, key_path, " + "unique_index, multientry " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "DROP TRIGGER object_data_insert_trigger;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_data (object_store_id, key_value, data, file_ids) " + "SELECT object_store_id, id, data, file_ids " + "FROM ai_object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "UPDATE object_store " + "SET auto_increment = (SELECT max(id) FROM ai_object_data) + 1 " + "WHERE auto_increment;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_object_data;"_ns); + 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() = default; + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override { + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + AUTO_PROFILER_LABEL("EncodeKeysFunction::OnFunctionCall", DOM); + + 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; + } + + QM_TRY_INSPECT( + const auto& key, ([type, aArguments]() -> Result<Key, nsresult> { + switch (type) { + case mozIStorageStatement::VALUE_TYPE_INTEGER: { + int64_t intKey; + aArguments->GetInt64(0, &intKey); + + Key key; + QM_TRY(key.SetFromInteger(intKey)); + + return key; + } + case mozIStorageStatement::VALUE_TYPE_TEXT: { + nsString stringKey; + aArguments->GetString(0, stringKey); + + Key key; + QM_TRY(key.SetFromString(stringKey)); + + return key; + } + default: + NS_WARNING("Don't call me with the wrong type of arguments!"); + return Err(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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom11_0To12_0", DOM); + + constexpr auto encoderName = "encode"_ns; + + nsCOMPtr<mozIStorageFunction> encoder = new EncodeKeysFunction(); + + nsresult rv = aConnection.CreateFunction(encoderName, 1, encoder); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "key_value, " + "data, " + "file_ids " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, encode(key_value), data, file_ids " + "FROM object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_data " + "SELECT id, object_store_id, key_value, file_ids, data " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT index_id, encode(value), encode(object_data_key), object_data_id " + "FROM index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX index_data_object_data_id_index " + "ON index_data (object_data_id);"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO temp_upgrade " + "SELECT index_id, encode(value), encode(object_data_key), object_data_id " + "FROM unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO unique_index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX unique_index_data_object_data_id_index " + "ON unique_index_data (object_data_id);"_ns); + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom12_0To13_0", DOM); + + 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(); + + // 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() = delete; + ~UpgradeSchemaFrom17_0To18_0Helper() = delete; +}; + +class UpgradeSchemaFrom17_0To18_0Helper::InsertIndexDataValuesFunction final + : public mozIStorageFunction { + public: + InsertIndexDataValuesFunction() = default; + + NS_DECL_ISUPPORTS + + private: + ~InsertIndexDataValuesFunction() = default; + + 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. + QM_TRY_UNWRAP(auto indexValues, ReadCompressedIndexDataValues(*aValues, 0)); + + IndexOrObjectStoreId indexId; + nsresult 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.InsertElementSorted( + IndexDataValue(indexId, !!unique, value), fallible))) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_OUT_OF_MEMORY; + } + + // Compress the array. + QM_TRY_UNWRAP((auto [indexValuesBlob, indexValuesBlobLength]), + MakeCompressedIndexDataValues(indexValues)); + + // The compressed blob is the result of this function. + nsCOMPtr<nsIVariant> result = new storage::AdoptedBlobVariant( + std::pair(indexValuesBlob.release(), indexValuesBlobLength)); + + result.forget(_retval); + return NS_OK; +} + +class UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction final + : public mozIStorageFunction { + public: + UpgradeKeyFunction() = default; + + 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() = default; + + 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); + + aDestination = std::copy(aSource, aSource + byteCount, aDestination); + aSource += byteCount; + + 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); + + aDestination = std::copy(aSource, aSource + byteCount, aDestination); + aSource += byteCount; + } + } + + 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(!aOrigin.IsEmpty()); + + // Register the |upgrade_key| function. + RefPtr<UpgradeKeyFunction> updateFunction = new UpgradeKeyFunction(); + + constexpr auto upgradeKeyFunctionName = "upgrade_key"_ns; + + 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(); + + constexpr auto insertIDVFunctionName = "insert_idv"_ns; + + 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(!aOrigin.IsEmpty()); + + nsresult rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Create the new tables and triggers first. + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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" + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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) " + ");"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + 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( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // The trigger is no longer needed. + "DROP TRIGGER unique_index_data_upgrade_insert_trigger;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // The old table is no longer needed. + "DROP TABLE unique_index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // Rename the table. + "ALTER TABLE unique_index_data_upgrade " + "RENAME TO unique_index_data;"_ns); + 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( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // The trigger is no longer needed. + "DROP TRIGGER index_data_upgrade_insert_trigger;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // The old table is no longer needed. + "DROP TABLE index_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // Rename the table. + "ALTER TABLE index_data_upgrade " + "RENAME TO index_data;"_ns); + 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( + // 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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // The temporary table is no longer needed. + "DROP TABLE temp_index_data_values;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // The old table is no longer needed. + "DROP TABLE object_data;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + // Rename the table. + "ALTER TABLE object_data_upgrade " + "RENAME TO object_data;"_ns); + 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( + "INSERT INTO object_store_index_upgrade " + "SELECT * " + "FROM object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE object_store_index_upgrade " + "RENAME TO object_store_index;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update the |object_store| table to remove the UNIQUE constraint. + rv = aConnection.ExecuteSimpleSQL( + "INSERT INTO object_store_upgrade " + "SELECT * " + "FROM object_store;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE object_store_upgrade " + "RENAME TO object_store;"_ns); + 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; + + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + rv = aConnection.CreateStatement( + "INSERT INTO database_upgrade " + "SELECT name, :origin, version, 0, 0, 0 " + "FROM database;"_ns, + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByIndex(0, 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("DROP TABLE database;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE database_upgrade " + "RENAME TO database;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + +#ifdef DEBUG + { + // Make sure there's only one entry in the |database| table. + QM_TRY_INSPECT(const auto& stmt, + quota::CreateAndExecuteSingleStepStatement( + aConnection, "SELECT COUNT(*) FROM database;"_ns), + QM_ASSERT_UNREACHABLE); + + int64_t count; + MOZ_ASSERT(NS_SUCCEEDED(stmt->GetInt64(0, &count))); + + MOZ_ASSERT(count == 1); + } +#endif + + // Recreate file table triggers. + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + 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 + "PRAGMA auto_vacuum = FULL;"_ns +#else + "PRAGMA auto_vacuum = INCREMENTAL;"_ns +#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(!aOrigin.IsEmpty()); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom17_0To18_0", DOM); + + return UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(aConnection, aOrigin); +} + +nsresult UpgradeSchemaFrom18_0To19_0(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + nsresult rv; + AUTO_PROFILER_LABEL("UpgradeSchemaFrom18_0To19_0", DOM); + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE object_store_index " + "ADD COLUMN locale TEXT;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE object_store_index " + "ADD COLUMN is_auto_locale BOOLEAN;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE index_data " + "ADD COLUMN value_locale BLOB;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "ALTER TABLE unique_index_data " + "ADD COLUMN value_locale BLOB;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "CREATE INDEX index_data_value_locale_index " + "ON index_data (index_id, value_locale, object_data_key, value) " + "WHERE value_locale IS NOT NULL;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + 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; +} + +class UpgradeFileIdsFunction final : public mozIStorageFunction { + SafeRefPtr<DatabaseFileManager> mFileManager; + + 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; +}; + +nsresult UpgradeSchemaFrom19_0To20_0(nsIFile* aFMDirectory, + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom19_0To20_0", DOM); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aConnection.CreateStatement( + "SELECT count(*) " + "FROM object_data " + "WHERE file_ids IS NOT NULL"_ns, + getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int64_t count; + + { + mozStorageStatementScoper scoper(stmt); + + QM_TRY_INSPECT(const bool& hasResult, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, ExecuteStep)); + + 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; + } + + constexpr auto functionName = "upgrade"_ns; + + rv = aConnection.CreateFunction(functionName, 2, function); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Disable update trigger. + rv = aConnection.ExecuteSimpleSQL( + "DROP TRIGGER object_data_update_trigger;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "UPDATE object_data " + "SET file_ids = upgrade(file_ids, data) " + "WHERE file_ids IS NOT NULL;"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Enable update trigger. + rv = aConnection.ExecuteSimpleSQL( + "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;"_ns); + 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; + } + + return NS_OK; +} + +class UpgradeIndexDataValuesFunction final : public mozIStorageFunction { + public: + UpgradeIndexDataValuesFunction() { AssertIsOnIOThread(); } + + NS_DECL_ISUPPORTS + + private: + ~UpgradeIndexDataValuesFunction() { AssertIsOnIOThread(); } + + using IndexDataValuesArray = IndexDataValuesAutoArray; + Result<IndexDataValuesArray, nsresult> ReadOldCompressedIDVFromBlob( + Span<const uint8_t> aBlobData); + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override; +}; + +NS_IMPL_ISUPPORTS(UpgradeIndexDataValuesFunction, mozIStorageFunction) + +Result<UpgradeIndexDataValuesFunction::IndexDataValuesArray, nsresult> +UpgradeIndexDataValuesFunction::ReadOldCompressedIDVFromBlob( + const Span<const uint8_t> aBlobData) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!IsOnBackgroundThread()); + + IndexOrObjectStoreId indexId; + bool unique; + bool nextIndexIdAlreadyRead = false; + + IndexDataValuesArray result; + for (auto remainder = aBlobData; !remainder.IsEmpty();) { + if (!nextIndexIdAlreadyRead) { + QM_TRY_UNWRAP((std::tie(indexId, unique, remainder)), + ReadCompressedIndexId(remainder)); + } + nextIndexIdAlreadyRead = false; + + if (NS_WARN_IF(remainder.IsEmpty())) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_FILE_CORRUPTED); + } + + // Read key buffer length. + QM_TRY_INSPECT( + (const auto& [keyBufferLength, remainderAfterKeyBufferLength]), + ReadCompressedNumber(remainder)); + + if (NS_WARN_IF(remainderAfterKeyBufferLength.IsEmpty()) || + NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) || + NS_WARN_IF(keyBufferLength > remainderAfterKeyBufferLength.Length())) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_FILE_CORRUPTED); + } + + const auto [keyBuffer, remainderAfterKeyBuffer] = + remainderAfterKeyBufferLength.SplitAt(keyBufferLength); + if (NS_WARN_IF(!result.EmplaceBack(fallible, indexId, unique, + Key{nsCString{AsChars(keyBuffer)}}))) { + IDB_REPORT_INTERNAL_ERR(); + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + remainder = remainderAfterKeyBuffer; + if (!remainder.IsEmpty()) { + // Read either a sort key buffer length or an index id. + QM_TRY_INSPECT((const auto& [maybeIndexId, remainderAfterIndexId]), + ReadCompressedNumber(remainder)); + + // 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. + // XXX This duplicates logic from ReadCompressedIndexId. + if (maybeIndexId != 0) { + unique = maybeIndexId % 2 == 1; + indexId = maybeIndexId / 2; + nextIndexIdAlreadyRead = true; + } + + remainder = remainderAfterIndexId; + } + } + result.Sort(); + + return result; +} + +NS_IMETHODIMP +UpgradeIndexDataValuesFunction::OnFunctionCall( + mozIStorageValueArray* aArguments, nsIVariant** aResult) { + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + AUTO_PROFILER_LABEL("UpgradeIndexDataValuesFunction::OnFunctionCall", DOM); + + 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; + } + + QM_TRY_INSPECT(const auto& oldIdv, + ReadOldCompressedIDVFromBlob(Span(oldBlob, oldBlobLength))); + + QM_TRY_UNWRAP((auto [newIdv, newIdvLength]), + MakeCompressedIndexDataValues(oldIdv)); + + nsCOMPtr<nsIVariant> result = new storage::AdoptedBlobVariant( + std::pair(newIdv.release(), newIdvLength)); + + 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(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom20_0To21_0", DOM); + + RefPtr<UpgradeIndexDataValuesFunction> function = + new UpgradeIndexDataValuesFunction(); + + constexpr auto functionName = "upgrade_idv"_ns; + + nsresult rv = aConnection.CreateFunction(functionName, 1, function); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "UPDATE object_data " + "SET index_data_values = upgrade_idv(index_data_values) " + "WHERE index_data_values IS NOT NULL;"_ns); + 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(!aOrigin.IsEmpty()); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom22_0To23_0", DOM); + + nsCOMPtr<mozIStorageStatement> stmt; + // The parameter names are not used, parameters are bound by index only + // locally in the same function. + nsresult rv = aConnection.CreateStatement( + "UPDATE database SET origin = :origin;"_ns, getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByIndex(0, 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; +} + +class StripObsoleteOriginAttributesFunction final : public mozIStorageFunction { + public: + NS_DECL_ISUPPORTS + + private: + ~StripObsoleteOriginAttributesFunction() = default; + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) override { + MOZ_ASSERT(aArguments); + MOZ_ASSERT(aResult); + + AUTO_PROFILER_LABEL("StripObsoleteOriginAttributesFunction::OnFunctionCall", + DOM); + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aArguments->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 1); + + int32_t type; + MOZ_ALWAYS_SUCCEEDS(aArguments->GetTypeOfIndex(0, &type)); + MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT); + } +#endif + + nsCString origin; + nsresult rv = aArguments->GetUTF8String(0, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Deserialize and re-serialize to automatically drop any obsolete origin + // attributes. + OriginAttributes oa; + + nsCString originNoSuffix; + bool ok = oa.PopulateFromOrigin(origin, originNoSuffix); + if (NS_WARN_IF(!ok)) { + return NS_ERROR_FAILURE; + } + + nsCString suffix; + oa.CreateSuffix(suffix); + + nsCOMPtr<nsIVariant> result = + new mozilla::storage::UTF8TextVariant(originNoSuffix + suffix); + + result.forget(aResult); + return NS_OK; + } +}; + +nsresult UpgradeSchemaFrom25_0To26_0(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("UpgradeSchemaFrom25_0To26_0", DOM); + + constexpr auto functionName = "strip_obsolete_attributes"_ns; + + nsCOMPtr<mozIStorageFunction> stripObsoleteAttributes = + new StripObsoleteOriginAttributesFunction(); + + nsresult rv = aConnection.CreateFunction(functionName, + /* aNumArguments */ 1, + stripObsoleteAttributes); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection.ExecuteSimpleSQL( + "UPDATE DATABASE " + "SET origin = strip_obsolete_attributes(origin) " + "WHERE origin LIKE '%^%';"_ns); + 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(26, 0)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(CompressDataBlobsFunction, mozIStorageFunction) +NS_IMPL_ISUPPORTS(EncodeKeysFunction, mozIStorageFunction) +NS_IMPL_ISUPPORTS(StripObsoleteOriginAttributesFunction, mozIStorageFunction); + +class DeserializeUpgradeValueHelper final : public Runnable { + public: + explicit DeserializeUpgradeValueHelper( + StructuredCloneReadInfoParent& aCloneReadInfo) + : Runnable("DeserializeUpgradeValueHelper"), + mMonitor("DeserializeUpgradeValueHelper::mMonitor"), + mCloneReadInfo(aCloneReadInfo), + mStatus(NS_ERROR_FAILURE) {} + + nsresult DispatchAndWait(nsAString& aFileIds) { + // We don't need to go to the main-thread and use the sandbox. + if (!mCloneReadInfo.Data().Size()) { + PopulateFileIds(aFileIds); + return NS_OK; + } + + // The operation will continue on the main-thread. + + MOZ_ASSERT(!(mCloneReadInfo.Data().Size() % sizeof(uint64_t))); + + MonitorAutoLock lock(mMonitor); + + RefPtr<Runnable> self = this; + const nsresult rv = + SchedulerGroup::Dispatch(TaskCategory::Other, self.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + lock.Wait(); + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + PopulateFileIds(aFileIds); + return NS_OK; + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + JS::Rooted<JSObject*> global(cx, GetSandbox(cx)); + if (NS_WARN_IF(!global)) { + OperationCompleted(NS_ERROR_FAILURE); + return NS_OK; + } + + const JSAutoRealm ar(cx, global); + + JS::Rooted<JS::Value> value(cx); + const nsresult rv = DeserializeUpgradeValue(cx, &value); + if (NS_WARN_IF(NS_FAILED(rv))) { + OperationCompleted(rv); + return NS_OK; + } + + OperationCompleted(NS_OK); + return NS_OK; + } + + private: + nsresult DeserializeUpgradeValue(JSContext* aCx, + JS::MutableHandle<JS::Value> aValue) { + static const JSStructuredCloneCallbacks callbacks = { + StructuredCloneReadCallback<StructuredCloneReadInfoParent>, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr}; + + if (!JS_ReadStructuredClone( + aCx, mCloneReadInfo.Data(), JS_STRUCTURED_CLONE_VERSION, + JS::StructuredCloneScope::DifferentProcessForIndexedDB, aValue, + JS::CloneDataPolicy(), &callbacks, &mCloneReadInfo)) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + return NS_OK; + } + + void PopulateFileIds(nsAString& aFileIds) { + for (uint32_t count = mCloneReadInfo.Files().Length(), index = 0; + index < count; index++) { + const StructuredCloneFileParent& file = mCloneReadInfo.Files()[index]; + + const int64_t id = file.FileInfo().Id(); + + if (index) { + aFileIds.Append(' '); + } + aFileIds.AppendInt(file.Type() == StructuredCloneFileBase::eBlob ? id + : -id); + } + } + + void OperationCompleted(nsresult aStatus) { + mStatus = aStatus; + + MonitorAutoLock lock(mMonitor); + lock.Notify(); + } + + Monitor mMonitor MOZ_UNANNOTATED; + StructuredCloneReadInfoParent& mCloneReadInfo; + nsresult mStatus; +}; + +nsresult DeserializeUpgradeValueToFileIds( + StructuredCloneReadInfoParent& aCloneReadInfo, nsAString& aFileIds) { + MOZ_ASSERT(!NS_IsMainThread()); + + const RefPtr<DeserializeUpgradeValueHelper> helper = + new DeserializeUpgradeValueHelper(aCloneReadInfo); + return helper->DispatchAndWait(aFileIds); +} + +nsresult UpgradeFileIdsFunction::Init(nsIFile* aFMDirectory, + mozIStorageConnection& aConnection) { + // This DatabaseFileManager doesn't need real origin info, etc. The only + // purpose is to store file ids without adding more complexity or code + // duplication. + auto fileManager = MakeSafeRefPtr<DatabaseFileManager>( + PERSISTENCE_TYPE_INVALID, quota::OriginMetadata{}, u""_ns, ""_ns, false, + false); + + nsresult rv = fileManager->Init(aFMDirectory, aConnection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mFileManager = std::move(fileManager); + 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); + + AUTO_PROFILER_LABEL("UpgradeFileIdsFunction::OnFunctionCall", DOM); + + 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; + } + + QM_TRY_UNWRAP(auto cloneInfo, GetStructuredCloneReadInfoFromValueArray( + aArguments, 1, 0, *mFileManager)); + + nsAutoString fileIds; + // XXX does this really need non-const cloneInfo? + rv = DeserializeUpgradeValueToFileIds(cloneInfo, fileIds); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_DATA_CLONE_ERR; + } + + nsCOMPtr<nsIVariant> result = new mozilla::storage::TextVariant(fileIds); + + result.forget(aResult); + return NS_OK; +} + +} // namespace + +Result<bool, nsresult> MaybeUpgradeSchema(mozIStorageConnection& aConnection, + const int32_t aSchemaVersion, + nsIFile& aFMDirectory, + const nsACString& aOrigin) { + bool vacuumNeeded = false; + int32_t schemaVersion = aSchemaVersion; + + // This logic needs to change next time we change the schema! + static_assert(kSQLiteSchemaVersion == int32_t((26 << 4) + 0), + "Upgrade function needed due to schema version increase."); + + while (schemaVersion != kSQLiteSchemaVersion) { + switch (schemaVersion) { + case 4: + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom4To5(aConnection))); + break; + case 5: + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom5To6(aConnection))); + break; + case 6: + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom6To7(aConnection))); + break; + case 7: + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom7To8(aConnection))); + break; + case 8: + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom8To9_0(aConnection))); + vacuumNeeded = true; + break; + case MakeSchemaVersion(9, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom9_0To10_0(aConnection))); + break; + case MakeSchemaVersion(10, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom10_0To11_0(aConnection))); + break; + case MakeSchemaVersion(11, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom11_0To12_0(aConnection))); + break; + case MakeSchemaVersion(12, 0): + QM_TRY(MOZ_TO_RESULT( + UpgradeSchemaFrom12_0To13_0(aConnection, &vacuumNeeded))); + break; + case MakeSchemaVersion(13, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom13_0To14_0(aConnection))); + break; + case MakeSchemaVersion(14, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom14_0To15_0(aConnection))); + break; + case MakeSchemaVersion(15, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom15_0To16_0(aConnection))); + break; + case MakeSchemaVersion(16, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom16_0To17_0(aConnection))); + break; + case MakeSchemaVersion(17, 0): + QM_TRY( + MOZ_TO_RESULT(UpgradeSchemaFrom17_0To18_0(aConnection, aOrigin))); + vacuumNeeded = true; + break; + case MakeSchemaVersion(18, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom18_0To19_0(aConnection))); + break; + case MakeSchemaVersion(19, 0): + QM_TRY(MOZ_TO_RESULT( + UpgradeSchemaFrom19_0To20_0(&aFMDirectory, aConnection))); + break; + case MakeSchemaVersion(20, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom20_0To21_0(aConnection))); + break; + case MakeSchemaVersion(21, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom21_0To22_0(aConnection))); + break; + case MakeSchemaVersion(22, 0): + QM_TRY( + MOZ_TO_RESULT(UpgradeSchemaFrom22_0To23_0(aConnection, aOrigin))); + break; + case MakeSchemaVersion(23, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom23_0To24_0(aConnection))); + break; + case MakeSchemaVersion(24, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom24_0To25_0(aConnection))); + break; + case MakeSchemaVersion(25, 0): + QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom25_0To26_0(aConnection))); + break; + default: + QM_FAIL(Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR), []() { + IDB_WARNING( + "Unable to open IndexedDB database, no upgrade path is " + "available!"); + }); + } + + QM_TRY_UNWRAP(schemaVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + } + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + + return vacuumNeeded; +} + +} // namespace mozilla::dom::indexedDB + +#undef IDB_MOBILE diff --git a/dom/indexedDB/SchemaUpgrades.h b/dom/indexedDB/SchemaUpgrades.h new file mode 100644 index 0000000000..8caec17d49 --- /dev/null +++ b/dom/indexedDB/SchemaUpgrades.h @@ -0,0 +1,28 @@ +/* -*- 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 dom_indexeddb_schemaupgrades_h__ +#define dom_indexeddb_schemaupgrades_h__ + +#include <cstdint> + +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "nsStringFwd.h" + +class mozIStorageConnection; +class nsIFile; + +namespace mozilla::dom::indexedDB { + +Result<bool, nsresult> MaybeUpgradeSchema(mozIStorageConnection& aConnection, + int32_t aSchemaVersion, + nsIFile& aFMDirectory, + const nsACString& aOrigin); + +} // namespace mozilla::dom::indexedDB + +#endif diff --git a/dom/indexedDB/ScriptErrorHelper.cpp b/dom/indexedDB/ScriptErrorHelper.cpp new file mode 100644 index 0000000000..c867a6654d --- /dev/null +++ b/dom/indexedDB/ScriptErrorHelper.cpp @@ -0,0 +1,191 @@ +/* -*- 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 "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +#include "mozilla/SchedulerGroup.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) + : mozilla::Runnable("ScriptErrorRunnable"), + 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) + : mozilla::Runnable("ScriptErrorRunnable"), + 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()); + + nsAutoString 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); + + nsCOMPtr<nsIScriptError> scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + // We may not be able to create the script error object when we're shutting + // down. + if (!scriptError) { + return; + } + + if (aInnerWindowID) { + MOZ_ALWAYS_SUCCEEDS(scriptError->InitWithWindowID( + aMessage, aFilename, + /* aSourceLine */ u""_ns, aLineNumber, aColumnNumber, aSeverityFlag, + category, aInnerWindowID)); + } else { + MOZ_ALWAYS_SUCCEEDS(scriptError->Init( + aMessage, aFilename, + /* aSourceLine */ u""_ns, aLineNumber, aColumnNumber, aSeverityFlag, + category, + /* IDB doesn't run on Private browsing mode */ false, + /* from chrome context */ aIsChrome)); + } + + // We may not be able to obtain the console service when we're shutting + // down. + if (consoleService) { + 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() = default; +}; + +} // namespace + +namespace mozilla::dom::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( + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget())); + } +} + +/*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( + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget())); + } +} + +} // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/ScriptErrorHelper.h b/dom/indexedDB/ScriptErrorHelper.h new file mode 100644 index 0000000000..691411b82f --- /dev/null +++ b/dom/indexedDB/ScriptErrorHelper.h @@ -0,0 +1,32 @@ +/* -*- 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__ + +#include <inttypes.h> +#include "nsStringFwd.h" + +namespace mozilla::dom::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 mozilla::dom::indexedDB + +#endif // mozilla_dom_indexeddb_scripterrorhelper_h__ diff --git a/dom/indexedDB/SerializationHelpers.h b/dom/indexedDB/SerializationHelpers.h new file mode 100644 index 0000000000..7bc05720d4 --- /dev/null +++ b/dom/indexedDB/SerializationHelpers.h @@ -0,0 +1,78 @@ +/* -*- 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/EnumSerializer.h" +#include "ipc/IPCMessageUtilsSpecializations.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::StructuredCloneFileBase::FileType> + : public ContiguousEnumSerializer< + mozilla::dom::indexedDB::StructuredCloneFileBase::FileType, + mozilla::dom::indexedDB::StructuredCloneFileBase::eBlob, + mozilla::dom::indexedDB::StructuredCloneFileBase::eEndGuard> {}; + +template <> +struct ParamTraits<mozilla::dom::indexedDB::Key> { + typedef mozilla::dom::indexedDB::Key paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mBuffer); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mBuffer); + } +}; + +template <> +struct ParamTraits<mozilla::dom::indexedDB::KeyPath::KeyPathType> + : public ContiguousEnumSerializer< + mozilla::dom::indexedDB::KeyPath::KeyPathType, + mozilla::dom::indexedDB::KeyPath::KeyPathType::NonExistent, + mozilla::dom::indexedDB::KeyPath::KeyPathType::EndGuard> {}; + +template <> +struct ParamTraits<mozilla::dom::indexedDB::KeyPath> { + typedef mozilla::dom::indexedDB::KeyPath paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mType); + WriteParam(aWriter, aParam.mStrings); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mType) && + ReadParam(aReader, &aResult->mStrings); + } +}; + +template <> +struct ParamTraits<mozilla::dom::IDBCursor::Direction> + : public ContiguousEnumSerializer< + mozilla::dom::IDBCursor::Direction, + mozilla::dom::IDBCursor::Direction::Next, + mozilla::dom::IDBCursor::Direction::EndGuard_> {}; + +template <> +struct ParamTraits<mozilla::dom::IDBTransaction::Mode> + : public ContiguousEnumSerializer< + mozilla::dom::IDBTransaction::Mode, + mozilla::dom::IDBTransaction::Mode::ReadOnly, + mozilla::dom::IDBTransaction::Mode::Invalid> {}; + +} // namespace IPC + +#endif // mozilla_dom_indexeddb_serializationhelpers_h__ diff --git a/dom/indexedDB/ThreadLocal.h b/dom/indexedDB/ThreadLocal.h new file mode 100644 index 0000000000..c780c9a250 --- /dev/null +++ b/dom/indexedDB/ThreadLocal.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_indexeddb_threadlocal_h__ +#define mozilla_dom_indexeddb_threadlocal_h__ + +#include "IDBTransaction.h" +#include "mozilla/dom/indexedDB/PBackgroundIDBSharedTypes.h" +#include "ProfilerHelpers.h" + +namespace mozilla::dom { + +class IDBFactory; + +namespace indexedDB { + +class ThreadLocal { + friend class DefaultDelete<ThreadLocal>; + friend IDBFactory; + + LoggingInfo mLoggingInfo; + Maybe<IDBTransaction&> mCurrentTransaction; + LoggingIdString<false> mLoggingIdString; + + NS_DECL_OWNINGTHREAD + + public: + ThreadLocal() = delete; + ThreadLocal(const ThreadLocal& aOther) = delete; + + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(ThreadLocal); } + + 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::Mode::VersionChange) { + return mLoggingInfo.nextVersionChangeTransactionSerialNumber()--; + } + + return mLoggingInfo.nextTransactionSerialNumber()++; + } + + uint64_t NextRequestSN() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mLoggingInfo.nextRequestSerialNumber() < UINT64_MAX); + + return mLoggingInfo.nextRequestSerialNumber()++; + } + + void SetCurrentTransaction(Maybe<IDBTransaction&> aCurrentTransaction) { + AssertIsOnOwningThread(); + + mCurrentTransaction = aCurrentTransaction; + } + + Maybe<IDBTransaction&> MaybeCurrentTransactionRef() const { + AssertIsOnOwningThread(); + + return mCurrentTransaction; + } + + private: + explicit ThreadLocal(const nsID& aBackgroundChildLoggingId); + ~ThreadLocal(); +}; + +} // namespace indexedDB +} // namespace mozilla::dom + +#endif // mozilla_dom_indexeddb_threadlocal_h__ diff --git a/dom/indexedDB/crashtests/1499854-1.html b/dom/indexedDB/crashtests/1499854-1.html new file mode 100644 index 0000000000..0e10601134 --- /dev/null +++ b/dom/indexedDB/crashtests/1499854-1.html @@ -0,0 +1,29 @@ +<html> + +<head> + <script> + function start() { + o1 = new Int32Array(51488) + o2 = new ArrayBuffer(13964) + for (let i = 0; i < 51488; i++) o1[i] = 0x41 + const dbRequest = window.indexedDB.open('', {}) + dbRequest.onupgradeneeded = function(event) { + const store = event.target.result.createObjectStore('IDBStore_0', {}) + store.add({}, 'ObjectKey_0') + store.add({ + data: undefined, + index_key_0: o1, + index_key_1: o2 + }, 'ObjectKey_1') + store.createIndex('IDBIndex_1', ['index_key_0', 'index_key_1'], { + unique: false, + multiEntry: false, + locale: 'fr' + }) + } + } + document.addEventListener('DOMContentLoaded', start) + </script> +</head> + +</html> diff --git a/dom/indexedDB/crashtests/1505821-1.html b/dom/indexedDB/crashtests/1505821-1.html new file mode 100644 index 0000000000..da4bd31ab3 --- /dev/null +++ b/dom/indexedDB/crashtests/1505821-1.html @@ -0,0 +1,16 @@ +<script> + const dbRequest = window.indexedDB.open('bug1505821_1_hello'); + dbRequest.onupgradeneeded = function (event) { + const store = event.target.result.createObjectStore('IDBStore_1', {autoIncrement: true}); + store.createIndex('I', [''], {unique: true}); + store.createIndex('J', ['a', ''], {unique: true}); + store.createIndex('M', ['', 'a'], {unique: true}); + store.createIndex('K', ['', 'a', ''], {unique: true}); + store.createIndex('L', ['', '', ''], {unique: true}); + } + + const dbRequest2 = window.indexedDB.open('bug1505821_1_hello'); + dbRequest.onsuccess = function (event) { + window.indexedDB.deleteDatabase("bug1507229_1_hello"); + } +</script> diff --git a/dom/indexedDB/crashtests/1543154-1.html b/dom/indexedDB/crashtests/1543154-1.html new file mode 100644 index 0000000000..b40121ed43 --- /dev/null +++ b/dom/indexedDB/crashtests/1543154-1.html @@ -0,0 +1,11 @@ +<script> + window.addEventListener('load', () => { + let a = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame') + document.documentElement.appendChild(a) + let b = a.contentWindow.indexedDB + while (document.documentElement.lastElementChild) { + document.documentElement.removeChild(document.documentElement.lastElementChild) + } + b.open('', { }) + }) +</script> diff --git a/dom/indexedDB/crashtests/1813284-1.html b/dom/indexedDB/crashtests/1813284-1.html new file mode 100644 index 0000000000..fd1d8987bc --- /dev/null +++ b/dom/indexedDB/crashtests/1813284-1.html @@ -0,0 +1,6 @@ +<script> + // This test allocates >= 4GB of memory even if it succeeds. + let a = new ArrayBuffer(4294967296) + let b = new Uint32Array(a) + self.indexedDB.cmp(b, undefined) +</script> diff --git a/dom/indexedDB/crashtests/726376-1.html b/dom/indexedDB/crashtests/726376-1.html new file mode 100644 index 0000000000..4187678fa8 --- /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 0000000000..2450ea9f85 --- /dev/null +++ b/dom/indexedDB/crashtests/crashtests.list @@ -0,0 +1,5 @@ +load 726376-1.html +load 1499854-1.html +load 1505821-1.html +load 1543154-1.html +skip-if(!is64Bit) skip-if(Android) skip-if(AddressSanitizer) skip-if(ThreadSanitizer) load 1813284-1.html diff --git a/dom/indexedDB/moz.build b/dom/indexedDB/moz.build new file mode 100644 index 0000000000..ca62d61ada --- /dev/null +++ b/dom/indexedDB/moz.build @@ -0,0 +1,120 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Storage: IndexedDB") + +MOCHITEST_MANIFESTS += [ + "test/mochitest-intl-api.ini", + "test/mochitest-private.ini", + "test/mochitest-regular.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", +] + +TEST_DIRS += ["test/gtest"] + +EXPORTS.mozilla.dom += [ + "DatabaseFileInfoFwd.h", + "FlippedOnce.h", + "IDBCursor.h", + "IDBCursorType.h", + "IDBDatabase.h", + "IDBEvents.h", + "IDBFactory.h", + "IDBIndex.h", + "IDBKeyRange.h", + "IDBObjectStore.h", + "IDBRequest.h", + "IDBTransaction.h", + "IndexedDatabase.h", + "IndexedDatabaseManager.h", + "SafeRefPtr.h", +] + +EXPORTS.mozilla.dom.indexedDB += [ + "ActorsChild.h", + "ActorsParent.h", + "IDBResult.h", + "Key.h", + "KeyPath.h", + "SerializationHelpers.h", + "ThreadLocal.h", +] + +UNIFIED_SOURCES += [ + "ActorsChild.cpp", + "ActorsParentCommon.cpp", + "DatabaseFileInfo.cpp", + "DBSchema.cpp", + "IDBCursor.cpp", + "IDBCursorType.cpp", + "IDBDatabase.cpp", + "IDBEvents.cpp", + "IDBFactory.cpp", + "IDBIndex.cpp", + "IDBKeyRange.cpp", + "IDBObjectStore.cpp", + "IDBRequest.cpp", + "IDBTransaction.cpp", + "IndexedDatabase.cpp", + "IndexedDatabaseManager.cpp", + "IndexedDBCipherKeyManager.cpp", + "IndexedDBCommon.cpp", + "KeyPath.cpp", + "ProfilerHelpers.cpp", + "ReportInternalError.cpp", + "SchemaUpgrades.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", + "PBackgroundIDBFactory.ipdl", + "PBackgroundIDBFactoryRequest.ipdl", + "PBackgroundIDBRequest.ipdl", + "PBackgroundIDBSharedTypes.ipdlh", + "PBackgroundIDBTransaction.ipdl", + "PBackgroundIDBVersionChangeTransaction.ipdl", + "PBackgroundIndexedDBUtils.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + # 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"] + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/storage", + "/ipc/glue", + "/third_party/sqlite3/src", + "/xpcom/build", +] + +XPIDL_SOURCES += [ + "nsIIDBPermissionsRequest.idl", +] + +XPIDL_MODULE = "dom_indexeddb" diff --git a/dom/indexedDB/nsIIDBPermissionsRequest.idl b/dom/indexedDB/nsIIDBPermissionsRequest.idl new file mode 100644 index 0000000000..4a85b19ac4 --- /dev/null +++ b/dom/indexedDB/nsIIDBPermissionsRequest.idl @@ -0,0 +1,24 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Interface for IDB permission requests. This is passed as the + * subject for the permission request observer service notifications. + */ +#include "nsISupports.idl" + +interface nsIObserver; + +webidl Element; + +[scriptable, builtinclass, uuid(c3493c65-0530-496e-995c-bcd38dbfce21)] +interface nsIIDBPermissionsRequest : nsISupports +{ + // The <browser> element the permission request is coming from. + readonly attribute Element browserElement; + + // The nsIObserver that can be used to send the reply notification. + readonly attribute nsIObserver responseObserver; +}; diff --git a/dom/indexedDB/test/.eslintrc.js b/dom/indexedDB/test/.eslintrc.js new file mode 100644 index 0000000000..4cb383ff7a --- /dev/null +++ b/dom/indexedDB/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + rules: { + "no-shadow": "off", + }, +}; diff --git a/dom/indexedDB/test/abort_on_reload.html b/dom/indexedDB/test/abort_on_reload.html new file mode 100644 index 0000000000..4e4fe3a339 --- /dev/null +++ b/dom/indexedDB/test/abort_on_reload.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> + +<body> + <script> + function createDb() { + return new Promise((resolve, reject) => { + const openRequest = indexedDB.open("test-abort-on-reload", 1); + openRequest.onsuccess = () => { + const db = openRequest.result; + // This would throw when db is corrupted. + db.transaction("databases", "readwrite"); + db.onversionchange = () => { + db.close(); + }; + resolve(); + }; + openRequest.onupgradeneeded = (evt) => { + // Interrupt upgrade + window.location.reload(); + opener.info('reload requested\n'); + openRequest.result.createObjectStore("databases"); + }; + }); + } + + function reset() { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase("test-abort-on-reload"); + request.onsuccess = resolve; + }); + } + + async function test() { + opener.postMessage("message", "*"); + + for (let i = 0; i < 10; ++i) { + opener.info(`iteration ${i}`); + await createDb(); + await reset(); + } + } + + test(); + </script> +</body> diff --git a/dom/indexedDB/test/bfcache_page1.html b/dom/indexedDB/test/bfcache_page1.html new file mode 100644 index 0000000000..e537d42008 --- /dev/null +++ b/dom/indexedDB/test/bfcache_page1.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <script> + var request = indexedDB.open(opener.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() { + opener.postMessage("go", "http://mochi.test:8888"); + }; + }; + </script> +</head> +<body> + This is page one. +</body> +</html> diff --git a/dom/indexedDB/test/bfcache_page2.html b/dom/indexedDB/test/bfcache_page2.html new file mode 100644 index 0000000000..464d74db51 --- /dev/null +++ b/dom/indexedDB/test/bfcache_page2.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> + <script> + var res = {}; + var request = indexedDB.open(opener.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() { + opener.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 0000000000..15d8277515 --- /dev/null +++ b/dom/indexedDB/test/blob_worker_crash_iframe.html @@ -0,0 +1,99 @@ +<!-- + 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 }, 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() { + /* eslint-env worker */ + 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.toString(), ")()"])); + + // 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 0000000000..abaa8ed97d --- /dev/null +++ b/dom/indexedDB/test/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +skip-if = (buildapp != "browser") +support-files = + head.js + browser_forgetThisSiteAdd.html + browser_forgetThisSiteGet.html + browserHelpers.js + bug839193.js + bug839193.xhtml + page_private_idb.html + +[browser_forgetThisSite.js] +skip-if = verify +[browser_private_idb.js] +skip-if = (os == 'mac' && debug) || (os == 'win' && debug) # Bug 1456325 +[browser_bug839193.js] +skip-if = os == 'win' && bits == 32 # Bug 1607975 diff --git a/dom/indexedDB/test/browserHelpers.js b/dom/indexedDB/test/browserHelpers.js new file mode 100644 index 0000000000..506303cb0d --- /dev/null +++ b/dom/indexedDB/test/browserHelpers.js @@ -0,0 +1,43 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// testSteps is expected to be defined by the file including this file. +/* global testSteps */ + +var testGenerator = testSteps(); + +var testResult; + +function runTest() { + testGenerator.next(); +} + +function finishTestNow() { + if (testGenerator) { + testGenerator.return(); + testGenerator = undefined; + } +} + +function finishTest() { + setTimeout(finishTestNow, 0); + setTimeout(() => { + window.parent.postMessage(testResult, "*"); + }, 0); +} + +function grabEventAndContinueHandler(event) { + testGenerator.next(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 0000000000..6d91452547 --- /dev/null +++ b/dom/indexedDB/test/browser_bug839193.js @@ -0,0 +1,44 @@ +/* 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.xhtml"); + 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"); + Services.obs.addObserver(onUnload, "bug839193-unloaded"); + + gBugWindow = window.openDialog(gTestRoot + "bug839193.xhtml"); +} diff --git a/dom/indexedDB/test/browser_forgetThisSite.js b/dom/indexedDB/test/browser_forgetThisSite.js new file mode 100644 index 0000000000..fd35069d84 --- /dev/null +++ b/dom/indexedDB/test/browser_forgetThisSite.js @@ -0,0 +1,62 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +let { ForgetAboutSite } = ChromeUtils.importESModule( + "resource://gre/modules/ForgetAboutSite.sys.mjs" +); + +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; + +add_task(async function test1() { + requestLongerTimeout(2); + // Avoids the prompt + setPermission(testPageURL1, "indexedDB"); + setPermission(testPageURL2, "indexedDB"); + + // Set database version for domain 1 + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, testPageURL1); + await waitForMessage(11, gBrowser); + gBrowser.removeCurrentTab(); +}); + +add_task(async function test2() { + // Set database version for domain 2 + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, testPageURL2); + await waitForMessage(11, gBrowser); + gBrowser.removeCurrentTab(); +}); + +add_task(async function test3() { + // Remove database from domain 2 + ForgetAboutSite.removeDataFromDomain(domains[1]).then(() => { + setPermission(testPageURL4, "indexedDB"); + }); +}); + +add_task(async function test4() { + // Get database version for domain 1 + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, testPageURL3); + await waitForMessage(11, gBrowser); + gBrowser.removeCurrentTab(); +}); + +add_task(async function test5() { + // Get database version for domain 2 + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, testPageURL4); + await waitForMessage(1, gBrowser); + gBrowser.removeCurrentTab(); +}); diff --git a/dom/indexedDB/test/browser_forgetThisSiteAdd.html b/dom/indexedDB/test/browser_forgetThisSiteAdd.html new file mode 100644 index 0000000000..5f71aaa59c --- /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"> + function* testSteps() + { + let request = indexedDB.open("browser_forgetThisSite.js", 11); + request.onerror = grabEventAndContinueHandler; + request.onupgradeneeded = grabEventAndContinueHandler; + let event = yield undefined; + + if (event.type == "error") { + testResult = 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" 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 0000000000..1d489234c0 --- /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"> + function* testSteps() + { + let request = indexedDB.open("browser_forgetThisSite.js"); + request.onerror = grabEventAndContinueHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + if (event.type == "error") { + testResult = event.target.error.name; + } + else { + let db = event.target.result; + testResult = db.version; + } + + finishTest(); + yield undefined; + } + </script> + + <script type="text/javascript" src="browserHelpers.js"></script> + + </head> + + <body onload="runTest();" onunload="finishTestNow();"></body> + +</html> diff --git a/dom/indexedDB/test/browser_private_idb.js b/dom/indexedDB/test/browser_private_idb.js new file mode 100644 index 0000000000..5627361fc6 --- /dev/null +++ b/dom/indexedDB/test/browser_private_idb.js @@ -0,0 +1,199 @@ +async function idbCheckFunc() { + let factory, console; + try { + // in a worker, this resolves directly. + factory = indexedDB; + console = self.console; + } catch (ex) { + // in a frame-script, we need to pierce "content" + factory = content.indexedDB; + console = content.console; + } + try { + console.log("opening db"); + const req = factory.open("db", 1); + const result = await new Promise((resolve, reject) => { + req.onerror = () => { + resolve("error"); + }; + // we expect the db to not exist and for created to resolve first + req.onupgradeneeded = () => { + resolve("created"); + }; + // ...so this will lose the race + req.onsuccess = event => { + resolve("already-exists"); + }; + }); + const db = req.result; + console.log("db req completed:", result); + if (result !== "error") { + db.close(); + console.log("deleting database"); + await new Promise((resolve, reject) => { + const delReq = factory.deleteDatabase("db"); + delReq.onerror = reject; + delReq.onsuccess = resolve; + }); + console.log("deleted database"); + } + return result; + } catch (ex) { + console.error("received error:", ex); + return "exception"; + } +} + +async function workerDriverFunc() { + const resultPromise = idbCheckFunc(); + /* eslint-env worker */ + // (SharedWorker) + if (!("postMessage" in self)) { + addEventListener("connect", function (evt) { + const port = evt.ports[0]; + resultPromise.then(result => { + console.log("worker test completed, postMessage-ing result:", result); + port.postMessage({ idbResult: result }); + }); + }); + } + const result = await resultPromise; + // (DedicatedWorker) + if ("postMessage" in self) { + console.log("worker test completed, postMessage-ing result:", result); + postMessage({ idbResult: result }); + } +} + +const workerScript = ` +${idbCheckFunc.toSource()} +(${workerDriverFunc.toSource()})(); +`; +const workerScriptBlob = new Blob([workerScript]); + +/** + * This function is deployed via ContextTask.spawn and operates in a tab + * frame script context. Its job is to create the worker that will run the + * idbCheckFunc and return the result to us. + */ +async function workerCheckDeployer({ srcBlob, workerType }) { + const { console } = content; + let worker, port; + const url = content.URL.createObjectURL(srcBlob); + if (workerType === "dedicated") { + worker = new content.Worker(url); + port = worker; + } else if (workerType === "shared") { + worker = new content.SharedWorker(url); + port = worker.port; + port.start(); + } else { + throw new Error("bad worker type!"); + } + + const result = await new Promise((resolve, reject) => { + port.addEventListener( + "message", + function (evt) { + resolve(evt.data.idbResult); + }, + { once: true } + ); + worker.addEventListener("error", function (evt) { + console.error("worker problem:", evt); + reject(evt); + }); + }); + console.log("worker completed test with result:", result); + + return result; +} + +function checkTabWindowIDB(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], idbCheckFunc); +} + +async function checkTabDedicatedWorkerIDB(tab) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + srcBlob: workerScriptBlob, + workerType: "dedicated", + }, + ], + workerCheckDeployer + ); +} + +async function checkTabSharedWorkerIDB(tab) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [ + { + srcBlob: workerScriptBlob, + workerType: "shared", + }, + ], + workerCheckDeployer + ); +} + +add_task(async function () { + const pageUrl = + "http://example.com/browser/dom/indexedDB/test/page_private_idb.html"; + + const enabled = SpecialPowers.getBoolPref( + "dom.indexedDB.privateBrowsing.enabled" + ); + + let normalWin = await BrowserTestUtils.openNewBrowserWindow(); + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let normalTab = await BrowserTestUtils.openNewForegroundTab( + normalWin.gBrowser, + pageUrl + ); + let privateTab = await BrowserTestUtils.openNewForegroundTab( + privateWin.gBrowser, + pageUrl + ); + + is( + await checkTabWindowIDB(normalTab), + "created", + "IndexedDB works in a non-private-browsing page." + ); + is( + await checkTabWindowIDB(privateTab), + enabled ? "created" : "error", + "IndexedDB does not work in a private-browsing page." + ); + + is( + await checkTabDedicatedWorkerIDB(normalTab), + "created", + "IndexedDB works in a non-private-browsing Worker." + ); + is( + await checkTabDedicatedWorkerIDB(privateTab), + enabled ? "created" : "error", + "IndexedDB does not work in a private-browsing Worker." + ); + + is( + await checkTabSharedWorkerIDB(normalTab), + "created", + "IndexedDB works in a non-private-browsing SharedWorker." + ); + is( + await checkTabSharedWorkerIDB(privateTab), + enabled ? "created" : "error", + "IndexedDB does not work in a private-browsing SharedWorker." + ); + + await BrowserTestUtils.closeWindow(normalWin); + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/dom/indexedDB/test/bug839193.js b/dom/indexedDB/test/bug839193.js new file mode 100644 index 0000000000..b5b951e32e --- /dev/null +++ b/dom/indexedDB/test/bug839193.js @@ -0,0 +1,29 @@ +/* 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 = Ci.nsIQuotaManagerService; + +var gURI = Services.io.newURI("http://localhost"); + +function onUsageCallback(request) {} + +function onLoad() { + var quotaManagerService = Cc[ + "@mozilla.org/dom/quota-manager-service;1" + ].getService(nsIQuotaManagerService); + let principal = Services.scriptSecurityManager.createContentPrincipal( + gURI, + {} + ); + var quotaRequest = quotaManagerService.getUsageForPrincipal( + principal, + onUsageCallback + ); + quotaRequest.cancel(); + Services.obs.notifyObservers(window, "bug839193-loaded"); +} + +function onUnload() { + Services.obs.notifyObservers(window, "bug839193-unloaded"); +} diff --git a/dom/indexedDB/test/bug839193.xhtml b/dom/indexedDB/test/bug839193.xhtml new file mode 100644 index 0000000000..ccda48f951 --- /dev/null +++ b/dom/indexedDB/test/bug839193.xhtml @@ -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 0000000000..9a426f4954 --- /dev/null +++ b/dom/indexedDB/test/chrome.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = chromeHelpers.js + +[test_globalObjects_chrome.xhtml] +[test_wrappedArray.xhtml] diff --git a/dom/indexedDB/test/chromeHelpers.js b/dom/indexedDB/test/chromeHelpers.js new file mode 100644 index 0000000000..8284181858 --- /dev/null +++ b/dom/indexedDB/test/chromeHelpers.js @@ -0,0 +1,40 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +// testSteps is expected to be defined by the file including this file. +/* global testSteps */ + +var testGenerator = testSteps(); + +if (!window.runTest) { + window.runTest = function () { + SimpleTest.waitForExplicitFinish(); + + testGenerator.next(); + }; +} + +function finishTest() { + SimpleTest.executeSoon(function () { + testGenerator.return(); + SimpleTest.finish(); + }); +} + +function grabEventAndContinueHandler(event) { + testGenerator.next(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 0000000000..672b7c3a5b --- /dev/null +++ b/dom/indexedDB/test/error_events_abort_transactions_iframe.html @@ -0,0 +1,239 @@ +<!-- + 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"> + + 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.next(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() { + 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(); + } + </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 0000000000..3788958014 --- /dev/null +++ b/dom/indexedDB/test/event_propagation_iframe.html @@ -0,0 +1,134 @@ +<!-- + 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"> + + let testGenerator = testSteps(); + + function ok(val, message) { + val = val ? "true" : "false"; + window.parent.postMessage("SimpleTest.ok(" + val + ", '" + message + + "');", "*"); + } + + function grabEventAndContinueHandler(event) { + testGenerator.next(event); + } + + function errorHandler(event) { + ok(false, "indexedDB error, code " + event.target.error.name); + finishTest(); + } + + function finishTest() { + testGenerator.return(); + window.parent.postMessage("SimpleTest.finish();", "*"); + } + + const eventChain = [ + "IDBRequest", + "IDBTransaction", + "IDBDatabase", + ]; + + let captureCount = 0; + let bubbleCount = 0; + let atTargetCount = 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 (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; + 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); + 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); + 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 0000000000..25a4f01e77 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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.next(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.return(); + 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 new Error("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 new Error("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 new Error("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/file.js b/dom/indexedDB/test/file.js new file mode 100644 index 0000000000..9116688cdd --- /dev/null +++ b/dom/indexedDB/test/file.js @@ -0,0 +1,237 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from helpers.js */ + +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 }); +} + +function getFile(name, type, view) { + return new File([view], name, { 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)); +} + +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) { + // eslint-disable-next-line mozilla/use-cc-etc + is(SpecialPowers.wrap(Blob).isInstance(blob1), true, "Instance of Blob"); + 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 verifyView(view1, view2) { + is(view1.byteLength, view2.byteLength, "Correct byteLength"); + verifyBuffers(view1, view2); + continueToNextStep(); +} + +function verifyWasmModule(module1, module2) { + // We assume the given modules have no imports and export a single function + // named 'run'. + var instance1 = new WebAssembly.Instance(module1); + var instance2 = new WebAssembly.Instance(module2); + is(instance1.exports.run(), instance2.exports.run(), "same run() result"); + + continueToNextStep(); +} + +function grabFileUsageAndContinueHandler(request) { + testGenerator.next(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* assertEventuallyHasFileInfo(name, id) { + yield* assertEventuallyWithGC( + () => utils.getFileReferences(name, id), + `Expect existing DatabaseFileInfo for ${name}/${id}` + ); +} + +function* assertEventuallyHasNoFileInfo(name, id) { + yield* assertEventuallyWithGC( + () => !utils.getFileReferences(name, id), + `Expect no existing DatabaseFileInfo for ${name}/${id}` + ); +} + +function* assertEventuallyFileRefCount(name, id, expectedCount) { + yield* assertEventuallyWithGC(() => { + let count = {}; + utils.getFileReferences(name, id, count); + return count.value == expectedCount; + }, `Expect ${expectedCount} existing references for ${name}/${id}`); +} + +function getFileDBRefCount(name, id) { + let count = {}; + utils.getFileReferences(name, id, {}, count); + return count.value; +} + +function flushPendingFileDeletions() { + utils.flushPendingFileDeletions(); +} diff --git a/dom/indexedDB/test/gtest/TestIDBResult.cpp b/dom/indexedDB/test/gtest/TestIDBResult.cpp new file mode 100644 index 0000000000..d20b68d4a6 --- /dev/null +++ b/dom/indexedDB/test/gtest/TestIDBResult.cpp @@ -0,0 +1,40 @@ +/* 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 "IDBResult.h" + +#include "gtest/gtest.h" + +using mozilla::ErrorResult; +using namespace mozilla::dom::indexedDB; + +TEST(IDBResultTest, ConstructWithValue) +{ + IDBResult<int, IDBSpecialValue::Failure> result(0); + EXPECT_FALSE(result.isErr() && + result.inspectErr().Is(SpecialValues::Failure)); + EXPECT_TRUE(result.isOk()); + EXPECT_EQ(result.unwrap(), 0); +} + +TEST(IDBResultTest, Expand) +{ + IDBResult<int, IDBSpecialValue::Failure> narrow{ + mozilla::Err(SpecialValues::Failure)}; + IDBResult<int, IDBSpecialValue::Failure, IDBSpecialValue::Invalid> wide{ + narrow.propagateErr()}; + EXPECT_TRUE(wide.isErr() && wide.inspectErr().Is(SpecialValues::Failure)); +} + +IDBResult<int, IDBSpecialValue::Failure> ThrowException() { + return mozilla::Err(IDBException(NS_ERROR_FAILURE)); +} + +TEST(IDBResultTest, ThrowException) +{ + auto result = ThrowException(); + EXPECT_TRUE(result.isErr() && + result.inspectErr().Is(SpecialValues::Exception)); + result.unwrapErr().AsException().SuppressException(); +} diff --git a/dom/indexedDB/test/gtest/TestKey.cpp b/dom/indexedDB/test/gtest/TestKey.cpp new file mode 100644 index 0000000000..8de788a2fc --- /dev/null +++ b/dom/indexedDB/test/gtest/TestKey.cpp @@ -0,0 +1,433 @@ +/* -*- 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 "gtest/gtest.h" + +#include "mozilla/dom/SimpleGlobalObject.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/indexedDB/Key.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/Unused.h" + +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject +#include "js/ArrayBuffer.h" +#include "js/PropertyAndElement.h" // JS_GetElement, JS_SetElement +#include "js/RootingAPI.h" +#include "js/String.h" +#include "js/TypeDecls.h" +#include "js/Value.h" + +// TODO: This PrintTo overload is defined in dom/media/gtest/TestGroupId.cpp. +// However, it is not used, probably because of +// https://stackoverflow.com/a/36941270 +void PrintTo(const nsString& value, std::ostream* os); + +using namespace mozilla; +using namespace mozilla::dom::indexedDB; +using JS::Rooted; + +// DOM_IndexedDB_Key_Ctor tests test the construction of a Key, and check the +// properties of the constructed key with the const methods afterwards. The +// tested ctors include the default ctor, which constructs an unset key, and the +// ctors that accepts an encoded buffer, which is then decoded using the +// Key::To* method corresponding to its type. +// +// So far, only some cases are tested: +// - scalar binary +// -- empty +// -- with 1-byte encoded representation +// - scalar string +// -- empty +// -- with 1-byte encoded representation +// +// TODO More test cases should be added, including +// - empty (?) +// - scalar binary +// -- containing 0 byte(s) +// -- with 2-byte encoded representation +// - scalar string +// -- with 2-byte and 3-byte encoded representation +// - scalar number +// - scalar date +// - arrays, incl. nested arrays, with various combinations of contained types + +TEST(DOM_IndexedDB_Key, Ctor_Default) +{ + auto key = Key{}; + + EXPECT_TRUE(key.IsUnset()); +} + +// TODO does such a helper function already exist? +template <size_t N> +static auto BufferAsCString(const uint8_t (&aBuffer)[N]) { + return nsCString{reinterpret_cast<const char*>( + static_cast<std::decay_t<const uint8_t[]>>(aBuffer)), + N}; +} + +static void ExpectKeyIsBinary(const Key& aKey) { + EXPECT_FALSE(aKey.IsUnset()); + + EXPECT_FALSE(aKey.IsArray()); + EXPECT_TRUE(aKey.IsBinary()); + EXPECT_FALSE(aKey.IsDate()); + EXPECT_FALSE(aKey.IsFloat()); + EXPECT_FALSE(aKey.IsString()); +} + +static void ExpectKeyIsString(const Key& aKey) { + EXPECT_FALSE(aKey.IsUnset()); + + EXPECT_FALSE(aKey.IsArray()); + EXPECT_FALSE(aKey.IsBinary()); + EXPECT_FALSE(aKey.IsDate()); + EXPECT_FALSE(aKey.IsFloat()); + EXPECT_TRUE(aKey.IsString()); +} + +static void ExpectKeyIsArray(const Key& aKey) { + EXPECT_FALSE(aKey.IsUnset()); + + EXPECT_TRUE(aKey.IsArray()); + EXPECT_FALSE(aKey.IsBinary()); + EXPECT_FALSE(aKey.IsDate()); + EXPECT_FALSE(aKey.IsFloat()); + EXPECT_FALSE(aKey.IsString()); +} + +static JSObject* ExpectArrayBufferObject(const JS::Value& aValue) { + EXPECT_TRUE(aValue.isObject()); + auto& object = aValue.toObject(); + EXPECT_TRUE(JS::IsArrayBufferObject(&object)); + return &object; +} + +static JSObject* ExpectArrayObject(JSContext* const aContext, + JS::Handle<JS::Value> aValue) { + EXPECT_TRUE(aValue.isObject()); + bool rv; + EXPECT_TRUE(JS::IsArrayObject(aContext, aValue, &rv)); + EXPECT_TRUE(rv); + return &aValue.toObject(); +} + +static void CheckArrayBuffer(const nsCString& aExpected, + const JS::Value& aActual) { + auto obj = ExpectArrayBufferObject(aActual); + size_t length; + bool isSharedMemory; + uint8_t* data; + JS::GetArrayBufferLengthAndData(obj, &length, &isSharedMemory, &data); + + EXPECT_EQ(aExpected.Length(), length); + EXPECT_EQ(0, memcmp(aExpected.get(), data, length)); +} + +static void CheckString(JSContext* const aContext, const nsString& aExpected, + JS::Handle<JS::Value> aActual) { + EXPECT_TRUE(aActual.isString()); + int32_t rv; + EXPECT_TRUE(JS_CompareStrings(aContext, + JS_NewUCStringCopyZ(aContext, aExpected.get()), + aActual.toString(), &rv)); + EXPECT_EQ(0, rv); +} + +namespace { +// This is modeled after dom/base/test/gtest/TestContentUtils.cpp +struct AutoTestJSContext { + AutoTestJSContext() + : mGlobalObject( + mozilla::dom::RootingCx(), + mozilla::dom::SimpleGlobalObject::Create( + mozilla::dom::SimpleGlobalObject::GlobalType::BindingDetail)) { + EXPECT_TRUE(mJsAPI.Init(mGlobalObject)); + mContext = mJsAPI.cx(); + } + + operator JSContext*() const { return mContext; } + + private: + Rooted<JSObject*> mGlobalObject; + mozilla::dom::AutoJSAPI mJsAPI; + JSContext* mContext; +}; + +// The following classes serve as base classes for the parametrized tests below. +// The name of each class reflects the parameter type. + +class TestWithParam_CString_ArrayBuffer_Pair + : public ::testing::TestWithParam<std::pair<nsCString, nsLiteralCString>> { +}; + +class TestWithParam_CString_String_Pair + : public ::testing::TestWithParam<std::pair<nsCString, nsLiteralString>> {}; + +class TestWithParam_LiteralString + : public ::testing::TestWithParam<nsLiteralString> {}; + +class TestWithParam_StringArray + : public ::testing::TestWithParam<std::vector<nsString>> {}; + +class TestWithParam_ArrayBufferArray + : public ::testing::TestWithParam<std::vector<nsCString>> {}; + +} // namespace + +TEST_P(TestWithParam_CString_ArrayBuffer_Pair, Ctor_EncodedBinary) { + const auto key = Key{GetParam().first}; + + ExpectKeyIsBinary(key); + + AutoTestJSContext context; + + Rooted<JS::Value> rv(context); + EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv)); + + CheckArrayBuffer(GetParam().second, rv); +} + +static const uint8_t zeroLengthBinaryEncodedBuffer[] = {Key::eBinary}; +static const uint8_t nonZeroLengthBinaryEncodedBuffer[] = {Key::eBinary, + 'a' + 1, 'b' + 1}; +INSTANTIATE_TEST_SUITE_P( + DOM_IndexedDB_Key, TestWithParam_CString_ArrayBuffer_Pair, + ::testing::Values( + std::make_pair(BufferAsCString(zeroLengthBinaryEncodedBuffer), ""_ns), + std::make_pair(BufferAsCString(nonZeroLengthBinaryEncodedBuffer), + "ab"_ns))); + +TEST_P(TestWithParam_CString_String_Pair, Ctor_EncodedString) { + const auto key = Key{GetParam().first}; + + ExpectKeyIsString(key); + + EXPECT_EQ(GetParam().second, key.ToString()); +} + +static const uint8_t zeroLengthStringEncodedBuffer[] = {Key::eString}; +static const uint8_t nonZeroLengthStringEncodedBuffer[] = {Key::eString, + 'a' + 1, 'b' + 1}; + +INSTANTIATE_TEST_SUITE_P( + DOM_IndexedDB_Key, TestWithParam_CString_String_Pair, + ::testing::Values( + std::make_pair(BufferAsCString(zeroLengthStringEncodedBuffer), u""_ns), + std::make_pair(BufferAsCString(nonZeroLengthStringEncodedBuffer), + u"ab"_ns))); + +TEST_P(TestWithParam_LiteralString, SetFromString) { + auto key = Key{}; + const auto result = key.SetFromString(GetParam()); + EXPECT_TRUE(result.isOk()); + + ExpectKeyIsString(key); + + EXPECT_EQ(GetParam(), key.ToString()); +} + +INSTANTIATE_TEST_SUITE_P(DOM_IndexedDB_Key, TestWithParam_LiteralString, + ::testing::Values(u""_ns, u"abc"_ns, u"\u007f"_ns, + u"\u0080"_ns, u"\u1fff"_ns, + u"\u7fff"_ns, u"\u8000"_ns, + u"\uffff"_ns)); + +static JS::Value CreateArrayBufferValue(JSContext* const aContext, + const size_t aSize, char* const aData) { + Rooted<JSObject*> arrayBuffer{ + aContext, JS::NewArrayBufferWithContents(aContext, aSize, aData)}; + EXPECT_TRUE(arrayBuffer); + return JS::ObjectValue(*arrayBuffer); +} + +// This tests calling SetFromJSVal with an ArrayBuffer scalar of length 0. +// TODO Probably there should be more test cases for SetFromJSVal with other +// ArrayBuffer scalars, which convert this into a parametrized test as well. +TEST(DOM_IndexedDB_Key, SetFromJSVal_ZeroLengthArrayBuffer) +{ + AutoTestJSContext context; + + auto key = Key{}; + Rooted<JS::Value> arrayBuffer(context, + CreateArrayBufferValue(context, 0, nullptr)); + const auto result = key.SetFromJSVal(context, arrayBuffer); + EXPECT_TRUE(result.isOk()); + + ExpectKeyIsBinary(key); + + Rooted<JS::Value> rv2(context); + EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv2)); + + CheckArrayBuffer(""_ns, rv2); +} + +template <typename CheckElement> +static void CheckArray(JSContext* const context, + JS::Handle<JS::Value> arrayValue, + const size_t expectedLength, + const CheckElement& checkElement) { + Rooted<JSObject*> actualArray(context, + ExpectArrayObject(context, arrayValue)); + + uint32_t actualLength; + EXPECT_TRUE(JS::GetArrayLength(context, actualArray, &actualLength)); + EXPECT_EQ(expectedLength, actualLength); + for (size_t i = 0; i < expectedLength; ++i) { + Rooted<JS::Value> element(static_cast<JSContext*>(context)); + EXPECT_TRUE(JS_GetElement(context, actualArray, i, &element)); + + checkElement(i, element); + } +} + +static JS::Value CreateArrayBufferArray( + JSContext* const context, const std::vector<nsCString>& elements) { + Rooted<JSObject*> arrayObject(context, + JS::NewArrayObject(context, elements.size())); + EXPECT_TRUE(arrayObject); + + Rooted<JS::Value> arrayBuffer(context); + for (size_t i = 0; i < elements.size(); ++i) { + // TODO strdup only works if the element is actually 0-terminated + arrayBuffer = CreateArrayBufferValue( + context, elements[i].Length(), + elements[i].Length() ? strdup(elements[i].get()) : nullptr); + EXPECT_TRUE(JS_SetElement(context, arrayObject, i, arrayBuffer)); + } + + return JS::ObjectValue(*arrayObject); +} + +TEST_P(TestWithParam_ArrayBufferArray, SetFromJSVal) { + const auto& elements = GetParam(); + + AutoTestJSContext context; + Rooted<JS::Value> arrayValue(context); + arrayValue = CreateArrayBufferArray(context, elements); + + auto key = Key{}; + const auto result = key.SetFromJSVal(context, arrayValue); + EXPECT_TRUE(result.isOk()); + + ExpectKeyIsArray(key); + + Rooted<JS::Value> rv2(context); + EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv2)); + + CheckArray(context, rv2, elements.size(), + [&elements](const size_t i, const JS::HandleValue& element) { + CheckArrayBuffer(elements[i], element); + }); +} + +const uint8_t element2[] = "foo"; +INSTANTIATE_TEST_SUITE_P( + DOM_IndexedDB_Key, TestWithParam_ArrayBufferArray, + testing::Values(std::vector<nsCString>{}, std::vector<nsCString>{""_ns}, + std::vector<nsCString>{""_ns, BufferAsCString(element2)})); + +static JS::Value CreateStringValue(JSContext* const context, + const nsString& string) { + JSString* str = JS_NewUCStringCopyZ(context, string.get()); + EXPECT_TRUE(str); + return JS::StringValue(str); +} + +static JS::Value CreateStringArray(JSContext* const context, + const std::vector<nsString>& elements) { + Rooted<JSObject*> array(context, + JS::NewArrayObject(context, elements.size())); + EXPECT_TRUE(array); + + for (size_t i = 0; i < elements.size(); ++i) { + Rooted<JS::Value> string(context, CreateStringValue(context, elements[i])); + EXPECT_TRUE(JS_SetElement(context, array, i, string)); + } + + return JS::ObjectValue(*array); +} + +TEST_P(TestWithParam_StringArray, SetFromJSVal) { + const auto& elements = GetParam(); + + AutoTestJSContext context; + Rooted<JS::Value> arrayValue(context, CreateStringArray(context, elements)); + + auto key = Key{}; + const auto result = key.SetFromJSVal(context, arrayValue); + EXPECT_TRUE(result.isOk()); + + ExpectKeyIsArray(key); + + Rooted<JS::Value> rv2(context); + EXPECT_EQ(NS_OK, key.ToJSVal(context, &rv2)); + + CheckArray( + context, rv2, elements.size(), + [&elements, &context](const size_t i, JS::Handle<JS::Value> element) { + CheckString(context, elements[i], element); + }); +} + +INSTANTIATE_TEST_SUITE_P( + DOM_IndexedDB_Key, TestWithParam_StringArray, + testing::Values(std::vector<nsString>{u""_ns, u"abc\u0080\u1fff"_ns}, + std::vector<nsString>{u"abc\u0080\u1fff"_ns, + u"abc\u0080\u1fff"_ns})); + +TEST(DOM_IndexedDB_Key, CompareKeys_NonZeroLengthArrayBuffer) +{ + AutoTestJSContext context; + const char buf[] = "abc\x80"; + + auto first = Key{}; + Rooted<JS::Value> arrayBuffer1( + context, CreateArrayBufferValue(context, sizeof buf, strdup(buf))); + const auto result1 = first.SetFromJSVal(context, arrayBuffer1); + EXPECT_TRUE(result1.isOk()); + + auto second = Key{}; + Rooted<JS::Value> arrayBuffer2( + context, CreateArrayBufferValue(context, sizeof buf, strdup(buf))); + const auto result2 = second.SetFromJSVal(context, arrayBuffer2); + EXPECT_TRUE(result2.isOk()); + + EXPECT_EQ(0, Key::CompareKeys(first, second)); +} + +constexpr auto kTestLocale = "e"_ns; + +TEST(DOM_IndexedDB_Key, ToLocaleAwareKey_Empty) +{ + const auto input = Key{}; + + auto res = input.ToLocaleAwareKey(kTestLocale); + EXPECT_TRUE(res.isOk()); + + EXPECT_TRUE(res.inspect().IsUnset()); +} + +TEST(DOM_IndexedDB_Key, ToLocaleAwareKey_Bug_1641598) +{ + const auto buffer = [] { + nsCString res; + // This is the encoded representation produced by the test case from bug + // 1641598. + res.AppendLiteral("\x90\x01\x01\x01\x01\x00\x40"); + for (const size_t unused : IntegerRange<size_t>(256)) { + Unused << unused; + res.AppendLiteral("\x01\x01\x80\x03\x43"); + } + return res; + }(); + const auto input = Key{buffer}; + + auto res = input.ToLocaleAwareKey(kTestLocale); + EXPECT_TRUE(res.isOk()); + + EXPECT_EQ(input, res.inspect()); +} diff --git a/dom/indexedDB/test/gtest/TestSafeRefPtr.cpp b/dom/indexedDB/test/gtest/TestSafeRefPtr.cpp new file mode 100644 index 0000000000..f3536db278 --- /dev/null +++ b/dom/indexedDB/test/gtest/TestSafeRefPtr.cpp @@ -0,0 +1,279 @@ +/* -*- 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 "gtest/gtest.h" + +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/RefCounted.h" + +using namespace mozilla; + +class SafeBase : public SafeRefCounted<SafeBase> { + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(SafeBase) + + SafeBase() : mDead(false) {} + + static inline int sNumDestroyed; + + ~SafeBase() { + MOZ_RELEASE_ASSERT(!mDead); + mDead = true; + sNumDestroyed++; + } + + private: + bool mDead; +}; +struct SafeDerived : public SafeBase {}; + +class Base : public RefCounted<Base> { + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(Base) + + Base() : mDead(false) {} + + static inline int sNumDestroyed; + + ~Base() { + MOZ_RELEASE_ASSERT(!mDead); + mDead = true; + sNumDestroyed++; + } + + private: + bool mDead; +}; + +struct Derived : public Base {}; + +already_AddRefed<Base> NewFoo() { + RefPtr<Base> ptr(new Base()); + return ptr.forget(); +} + +already_AddRefed<Base> NewBar() { + RefPtr<Derived> bar = new Derived(); + return bar.forget(); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_Default) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr; + ASSERT_FALSE(ptr); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(0, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromNullPtr) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = nullptr; + ASSERT_FALSE(ptr); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(0, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromMakeSafeRefPtr_SafeRefCounted) +{ + SafeBase::sNumDestroyed = 0; + { + SafeRefPtr<SafeBase> ptr = MakeSafeRefPtr<SafeBase>(); + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + } + ASSERT_EQ(1, SafeBase::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, + Construct_FromMakeSafeRefPtr_SafeRefCounted_DerivedType) +{ + SafeBase::sNumDestroyed = 0; + { + SafeRefPtr<SafeBase> ptr = MakeSafeRefPtr<SafeDerived>(); + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + } + ASSERT_EQ(1, SafeBase::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromMakeSafeRefPtr_RefCounted) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = MakeSafeRefPtr<Base>(); + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromRefPtr) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = SafeRefPtr{MakeRefPtr<Base>()}; + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromRefPtr_DerivedType) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = SafeRefPtr{MakeRefPtr<Derived>()}; + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromAlreadyAddRefed) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr1 = SafeRefPtr{NewFoo()}; + SafeRefPtr<Base> ptr2(NewFoo()); + ASSERT_TRUE(ptr1); + ASSERT_TRUE(ptr2); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(2, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromAlreadyAddRefed_DerivedType) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = SafeRefPtr{NewBar()}; + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromRawPtr) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = SafeRefPtr{new Base(), AcquireStrongRefFromRawPtr{}}; + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Construct_FromRawPtr_Dervied) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr = + SafeRefPtr{new Derived(), AcquireStrongRefFromRawPtr{}}; + ASSERT_TRUE(ptr); + ASSERT_EQ(1u, ptr->refCount()); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, ClonePtr) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr1; + { + ptr1 = MakeSafeRefPtr<Base>(); + const SafeRefPtr<Base> ptr2 = ptr1.clonePtr(); + SafeRefPtr<Base> f3 = ptr2.clonePtr(); + + ASSERT_EQ(3u, ptr1->refCount()); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(1u, ptr1->refCount()); + ASSERT_EQ(0, Base::sNumDestroyed); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Forget) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr1 = MakeSafeRefPtr<Base>(); + SafeRefPtr<Base> ptr2 = SafeRefPtr{ptr1.forget()}; + + ASSERT_FALSE(ptr1); + ASSERT_TRUE(ptr2); + ASSERT_EQ(1u, ptr2->refCount()); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, Downcast) +{ + Base::sNumDestroyed = 0; + { + SafeRefPtr<Base> ptr1 = MakeSafeRefPtr<Derived>(); + SafeRefPtr<Derived> ptr2 = std::move(ptr1).downcast<Derived>(); + + ASSERT_FALSE(ptr1); + ASSERT_TRUE(ptr2); + ASSERT_EQ(1u, ptr2->refCount()); + } + ASSERT_EQ(1, Base::sNumDestroyed); +} + +struct SafeTest final : SafeBase { + template <typename Func> + explicit SafeTest(Func aCallback) { + aCallback(SafeRefPtrFromThis()); + } +}; + +TEST(DOM_IndexedDB_SafeRefPtr, SafeRefPtrFromThis_StoreFromCtor) +{ + SafeBase::sNumDestroyed = 0; + { + SafeRefPtr<SafeBase> ptr1; + { + SafeRefPtr<SafeTest> ptr2 = + MakeSafeRefPtr<SafeTest>([&ptr1](SafeRefPtr<SafeBase> ptr) { + ptr1 = std::move(ptr); + EXPECT_EQ(2u, ptr1->refCount()); + }); + ASSERT_EQ(2u, ptr2->refCount()); + } + ASSERT_EQ(0, SafeBase::sNumDestroyed); + ASSERT_EQ(1u, ptr1->refCount()); + } + ASSERT_EQ(1, SafeBase::sNumDestroyed); +} + +TEST(DOM_IndexedDB_SafeRefPtr, SafeRefPtrFromThis_DiscardInCtor) +{ + SafeBase::sNumDestroyed = 0; + { + SafeRefPtr<SafeTest> ptr = MakeSafeRefPtr<SafeTest>( + [](SafeRefPtr<SafeBase> ptr) { EXPECT_EQ(2u, ptr->refCount()); }); + ASSERT_EQ(1u, ptr->refCount()); + ASSERT_EQ(0, SafeBase::sNumDestroyed); + } + ASSERT_EQ(1, SafeBase::sNumDestroyed); +} + +static_assert( + std::is_same_v<SafeRefPtr<Base>, + decltype(std::declval<bool>() ? MakeSafeRefPtr<Derived>() + : MakeSafeRefPtr<Base>())>); diff --git a/dom/indexedDB/test/gtest/TestSimpleFileInfo.cpp b/dom/indexedDB/test/gtest/TestSimpleFileInfo.cpp new file mode 100644 index 0000000000..8971ee54fb --- /dev/null +++ b/dom/indexedDB/test/gtest/TestSimpleFileInfo.cpp @@ -0,0 +1,278 @@ +/* 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 "FileInfoImpl.h" +#include "FileInfoManager.h" + +#include "gtest/gtest.h" + +#include "mozilla/ArrayAlgorithm.h" +#include "mozilla/StaticMutex.h" +#include "nsTArray.h" + +#include <array> + +using namespace mozilla; +using namespace mozilla::dom::indexedDB; + +class SimpleFileManager; + +using SimpleFileInfo = FileInfo<SimpleFileManager>; + +struct SimpleFileManagerStats final { + // XXX We don't keep track of the specific aFileId parameters here, should we? + + size_t mAsyncDeleteFileCalls = 0; + size_t mSyncDeleteFileCalls = 0; +}; + +class SimpleFileManager final : public FileInfoManager<SimpleFileManager>, + public AtomicSafeRefCounted<SimpleFileManager> { + public: + using FileInfoManager<SimpleFileManager>::MutexType; + + MOZ_DECLARE_REFCOUNTED_TYPENAME(SimpleFileManager) + + // SimpleFileManager functions that are used by SimpleFileInfo + + [[nodiscard]] nsresult AsyncDeleteFile(const int64_t aFileId) { + MOZ_RELEASE_ASSERT(!mFileInfos.Contains(aFileId)); + + if (mStats) { + ++mStats->mAsyncDeleteFileCalls; + } + + return NS_OK; + } + + [[nodiscard]] nsresult SyncDeleteFile(const int64_t aFileId) { + MOZ_RELEASE_ASSERT(!mFileInfos.Contains(aFileId)); + + if (mStats) { + ++mStats->mSyncDeleteFileCalls; + } + return NS_OK; + } + + // Test-specific functions + explicit SimpleFileManager(SimpleFileManagerStats* aStats = nullptr) + : mStats{aStats} {} + + void CreateDBOnlyFileInfos() { + for (const auto id : kDBOnlyFileInfoIds) { + // Copied from within DatabaseFileManager::Init. + + mFileInfos.InsertOrUpdate( + id, MakeNotNull<SimpleFileInfo*>(FileInfoManagerGuard{}, + SafeRefPtrFromThis(), id, + static_cast<nsrefcnt>(1))); + + mLastFileId = std::max(id, mLastFileId); + } + } + + static MutexType& Mutex() { return sMutex; } + + static constexpr auto kDBOnlyFileInfoIds = + std::array<int64_t, 3>{{10, 20, 30}}; + + private: + inline static MutexType sMutex; + + SimpleFileManagerStats* const mStats; +}; + +// These tests test the SimpleFileManager itself, to ensure the SimpleFileInfo +// tests below are valid. + +TEST(DOM_IndexedDB_SimpleFileManager, Invalidate) +{ + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(); + + fileManager->Invalidate(); + + ASSERT_TRUE(fileManager->Invalidated()); +} + +// These tests mainly test SimpleFileInfo, which is a simplified version of +// DatabaseFileInfo (SimpleFileInfo doesn't work with real files stored on +// disk). The actual objects, DatabaseFileInfo and DatabaseFileManager are not +// tested here. + +TEST(DOM_IndexedDB_SimpleFileInfo, Create) +{ + auto stats = SimpleFileManagerStats{}; + + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + auto fileInfo = fileManager->CreateFileInfo(); + + int32_t memRefCnt, dbRefCnt; + fileInfo->GetReferences(&memRefCnt, &dbRefCnt); + + ASSERT_EQ(fileManager, &fileInfo->Manager()); + + ASSERT_EQ(1, memRefCnt); + ASSERT_EQ(0, dbRefCnt); + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + ASSERT_EQ(1u, stats.mAsyncDeleteFileCalls); +} + +TEST(DOM_IndexedDB_SimpleFileInfo, CreateWithInitialDBRefCnt) +{ + auto stats = SimpleFileManagerStats{}; + + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + fileManager->CreateDBOnlyFileInfos(); + + for (const auto id : SimpleFileManager::kDBOnlyFileInfoIds) { + const auto fileInfo = fileManager->GetFileInfo(id); + ASSERT_NE(nullptr, fileInfo); + + int32_t memRefCnt, dbRefCnt; + fileInfo->GetReferences(&memRefCnt, &dbRefCnt); + + ASSERT_EQ(fileManager, &fileInfo->Manager()); + + ASSERT_EQ(1, memRefCnt); // we hold one in fileInfo ourselves + ASSERT_EQ(1, dbRefCnt); + } + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + // Since the files have still non-zero dbRefCnt, nothing must be deleted. + ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls); +} + +TEST(DOM_IndexedDB_SimpleFileInfo, CreateWithInitialDBRefCnt_Invalidate) +{ + auto stats = SimpleFileManagerStats{}; + + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + fileManager->CreateDBOnlyFileInfos(); + + const auto fileInfos = TransformIntoNewArray( + SimpleFileManager::kDBOnlyFileInfoIds, + [&fileManager](const auto id) { return fileManager->GetFileInfo(id); }); + + fileManager->Invalidate(); + + for (const auto& fileInfo : fileInfos) { + int32_t memRefCnt, dbRefCnt; + fileInfo->GetReferences(&memRefCnt, &dbRefCnt); + + ASSERT_EQ(1, memRefCnt); // we hold one in fileInfo ourselves + ASSERT_EQ(0, dbRefCnt); // dbRefCnt was cleared by Invalidate + } + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + // Since the files have still non-zero dbRefCnt, nothing must be deleted. + ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls); +} + +TEST(DOM_IndexedDB_SimpleFileInfo, CreateWithInitialDBRefCnt_UpdateDBRefsToZero) +{ + auto stats = SimpleFileManagerStats{}; + + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + fileManager->CreateDBOnlyFileInfos(); + + const auto fileInfo = + fileManager->GetFileInfo(SimpleFileManager::kDBOnlyFileInfoIds[0]); + fileInfo->UpdateDBRefs(-1); + + int32_t memRefCnt, dbRefCnt; + fileInfo->GetReferences(&memRefCnt, &dbRefCnt); + + ASSERT_EQ(1, memRefCnt); // we hold one in fileInfo ourselves + ASSERT_EQ(0, dbRefCnt); + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + ASSERT_EQ(1u, stats.mAsyncDeleteFileCalls); +} + +TEST(DOM_IndexedDB_SimpleFileInfo, ReleaseWithFileManagerCleanup) +{ + auto stats = SimpleFileManagerStats{}; + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + fileManager->CreateDBOnlyFileInfos(); + + auto* fileInfo = fileManager->CreateFileInfo().forget().take(); + fileInfo->Release(/* aSyncDeleteFile */ true); + + // This was the only reference and SimpleFileManager was not invalidated, + // so SimpleFileManager::Cleanup should have been called. + ASSERT_EQ(1u, stats.mSyncDeleteFileCalls); + } + ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls); +} + +#ifndef DEBUG +// These tests cause assertion failures in DEBUG builds. + +TEST(DOM_IndexedDB_SimpleFileInfo, Invalidate_CreateFileInfo) +{ + auto stats = SimpleFileManagerStats{}; + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + + fileManager->Invalidate(); + + const auto fileInfo = fileManager->CreateFileInfo(); + Unused << fileInfo; + + ASSERT_EQ(nullptr, fileInfo); + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls); +} +#endif + +TEST(DOM_IndexedDB_SimpleFileInfo, Invalidate_Release) +{ + auto stats = SimpleFileManagerStats{}; + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + + const auto fileInfo = fileManager->CreateFileInfo(); + Unused << fileInfo; + + fileManager->Invalidate(); + + // SimpleFileManager was invalidated, so Release does not do any cleanup. + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls); +} + +TEST(DOM_IndexedDB_SimpleFileInfo, Invalidate_ReleaseWithFileManagerCleanup) +{ + auto stats = SimpleFileManagerStats{}; + { + const auto fileManager = MakeSafeRefPtr<SimpleFileManager>(&stats); + + auto* fileInfo = fileManager->CreateFileInfo().forget().take(); + + fileManager->Invalidate(); + + // SimpleFileManager was invalidated, so Release does not do any cleanup. + fileInfo->Release(/* aSyncDeleteFile */ true); + } + + ASSERT_EQ(0u, stats.mSyncDeleteFileCalls); + ASSERT_EQ(0u, stats.mAsyncDeleteFileCalls); +} + +// XXX Add test for GetFileForFileInfo diff --git a/dom/indexedDB/test/gtest/moz.build b/dom/indexedDB/test/gtest/moz.build new file mode 100644 index 0000000000..2005a813f3 --- /dev/null +++ b/dom/indexedDB/test/gtest/moz.build @@ -0,0 +1,23 @@ +# 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/. + +UNIFIED_SOURCES = [ + "TestIDBResult.cpp", +] + +# not UNIFIED_SOURCES because TestKey.cpp has classes in an anonymous namespace +# which result in a GCC error when used in tests, cf. gfx/tests/gtest/moz.build +SOURCES = [ + "TestKey.cpp", + "TestSafeRefPtr.cpp", + "TestSimpleFileInfo.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/dom/indexedDB", +] diff --git a/dom/indexedDB/test/head.js b/dom/indexedDB/test/head.js new file mode 100644 index 0000000000..802f093c6b --- /dev/null +++ b/dom/indexedDB/test/head.js @@ -0,0 +1,153 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gActiveListeners = {}; + +// These event (un)registration handlers only work for one window, DONOT use +// them with multiple windows. +function registerPopupEventHandler(eventName, callback, win) { + if (!win) { + win = window; + } + gActiveListeners[eventName] = function (event) { + if (event.target != win.PopupNotifications.panel) { + return; + } + win.PopupNotifications.panel.removeEventListener( + eventName, + gActiveListeners[eventName] + ); + delete gActiveListeners[eventName]; + + callback.call(win.PopupNotifications.panel); + }; + win.PopupNotifications.panel.addEventListener( + eventName, + gActiveListeners[eventName] + ); +} + +function unregisterPopupEventHandler(eventName, win) { + if (!win) { + win = window; + } + win.PopupNotifications.panel.removeEventListener( + eventName, + gActiveListeners[eventName] + ); + delete gActiveListeners[eventName]; +} + +function unregisterAllPopupEventHandlers(win) { + if (!win) { + win = window; + } + for (let eventName in gActiveListeners) { + win.PopupNotifications.panel.removeEventListener( + eventName, + gActiveListeners[eventName] + ); + } + gActiveListeners = {}; +} + +function triggerMainCommand(popup) { + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +function triggerSecondaryCommand(popup, win) { + if (!win) { + win = window; + } + info("triggering secondary command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {}, win); +} + +function dismissNotification(popup) { + info("dismissing notification"); + executeSoon(function () { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} + +function waitForMessage(aMessage, browser) { + // We cannot capture aMessage inside the checkFn, so we override the + // checkFn.toSource to tunnel aMessage instead. + let checkFn = function () {}; + checkFn.toSource = function () { + return `function checkFn(event) { + let message = ${aMessage.toSource()}; + if (event.data == message) { + return true; + } + throw new Error( + \`Unexpected result: \$\{event.data\}, expected \$\{message\}\` + ); + }`; + }; + + return BrowserTestUtils.waitForContentEvent( + browser.selectedBrowser, + "message", + /* capture */ true, + checkFn, + /* wantsUntrusted */ true + ).then(() => { + // An assertion in checkFn wouldn't be recorded as part of the test, so we + // use this assertion to confirm that we've successfully received the + // message (we'll only reach this point if that's the case). + ok(true, "Received message: " + aMessage); + }); +} + +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, originAttributes = {}) { + let uri = Services.io.newURI(url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + originAttributes + ); + + Services.perms.addFromPrincipal( + principal, + permission, + Ci.nsIPermissionManager.ALLOW_ACTION + ); +} + +function removePermission(url, permission, originAttributes = {}) { + let uri = Services.io.newURI(url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + originAttributes + ); + + Services.perms.removeFromPrincipal(principal, permission); +} + +function getPermission(url, permission, originAttributes = {}) { + let uri = Services.io.newURI(url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + originAttributes + ); + + return Services.perms.testPermissionFromPrincipal(principal, permission); +} diff --git a/dom/indexedDB/test/helpers.js b/dom/indexedDB/test/helpers.js new file mode 100644 index 0000000000..031b57af74 --- /dev/null +++ b/dom/indexedDB/test/helpers.js @@ -0,0 +1,851 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// testSteps is expected to be defined by the test using this file. +/* global testSteps:false */ + +var testGenerator; +if (testSteps.constructor.name === "GeneratorFunction") { + testGenerator = testSteps(); +} +// 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 || !c.value || c.writable) && typeof SpecialPowers === "object") { + // eslint-disable-next-line no-global-assign + Components = SpecialPowers.wrap(SpecialPowers.Components); +} + +function executeSoon(aFun) { + SpecialPowers.Services.tm.dispatchToMainThread({ + run() { + aFun(); + }, + }); +} + +function clearAllDatabases(callback) { + let qms = SpecialPowers.Services.qms; + let principal = SpecialPowers.wrap(document).effectiveStoragePrincipal; + let request = qms.clearStoragesForPrincipal(principal); + let cb = SpecialPowers.wrapCallback(callback); + request.callback = cb; +} + +var testHarnessGenerator = testHarnessSteps(); +testHarnessGenerator.next(); + +function* testHarnessSteps() { + function nextTestHarnessStep(val) { + testHarnessGenerator.next(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], + ["javascript.options.wasm_baselinejit", true], // This can be removed when on by default + ], + }, + 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) { + // For the AsyncFunction, handle the executing sequece using + // add_task(). For the GeneratorFunction, we just handle the sequence + // manually. + if (testSteps.constructor.name === "AsyncFunction") { + add_task(function workerTestSteps() { + return executeWorkerTestAndCleanUp(testScriptPath); + }); + } else { + ok( + testSteps.constructor.name === "GeneratorFunction", + "Unsupported function type" + ); + executeWorkerTestAndCleanUp(testScriptPath).then(nextTestHarnessStep); + + yield undefined; + } + } else if (testScriptFilename) { + todo( + false, + "Skipping test in a worker because it is explicitly disabled: " + + window.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. + if (testSteps.constructor.name === "AsyncFunction") { + // Register a callback to clean up databases because it's the only way for + // add_task() to clean them right before the SimpleTest.FinishTest + SimpleTest.registerCleanupFunction(async function () { + await new Promise(function (resolve, reject) { + clearAllDatabases(function (result) { + if (result.resultCode == SpecialPowers.Cr.NS_OK) { + resolve(result); + } else { + reject(result.resultCode); + } + }); + }); + }); + + add_task(testSteps); + } else { + testGenerator.next(); + + yield undefined; + } +} + +if (!window.runTest) { + window.runTest = function () { + SimpleTest.waitForExplicitFinish(); + testHarnessGenerator.next(); + }; +} + +function finishTest() { + ok( + testSteps.constructor.name === "GeneratorFunction", + "Async/await tests shouldn't call finishTest()" + ); + SimpleTest.executeSoon(function () { + clearAllDatabases(function () { + SimpleTest.finish(); + }); + }); +} + +function browserRunTest() { + testGenerator.next(); +} + +function browserFinishTest() {} + +function grabEventAndContinueHandler(event) { + testGenerator.next(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(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; + } + + if (_k1_ instanceof ArrayBuffer) { + if (!(_k2_ instanceof ArrayBuffer)) { + return false; + } + + function arrayBuffersAreEqual(a, b) { + if (a.byteLength != b.byteLength) { + return false; + } + let ui8b = new Uint8Array(b); + return new Uint8Array(a).every((val, i) => val === ui8b[i]); + } + + return arrayBuffersAreEqual(_k1_, _k2_); + } + + 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); +} + +// Assert that eventually a condition becomes true, running a garbage +// collection between evaluations. Fails, after a high number of iterations +// without a successful evaluation of the condition. +function* assertEventuallyWithGC(conditionFunctor, message) { + const maxGC = 100; + for (let i = 0; i < maxGC; ++i) { + if (conditionFunctor()) { + ok(true, message + " (after " + i + " garbage collections)"); + return; + } + SpecialPowers.exactGC(continueToNextStep); + yield undefined; + } + ok(false, message + " (even after " + maxGC + " garbage collections)"); +} + +// Asserts that a functor `f` throws an exception that is an instance of +// `ctor`. If it doesn't throw, or throws a different type of exception, this +// throws an Error, including the optional `msg` given. +// Otherwise, it returns the message of the exception. +// +// TODO This is DUPLICATED from https://searchfox.org/mozilla-central/rev/cfd1cc461f1efe0d66c2fdc17c024a203d5a2fd8/js/src/tests/shell.js#163 +// This should be moved to a more generic place, as it is in no way specific +// to IndexedDB. +function assertThrowsInstanceOf(f, ctor, msg) { + var fullmsg; + try { + f(); + } catch (exc) { + if (exc instanceof ctor) { + return exc.message; + } + fullmsg = `Assertion failed: expected exception ${ctor.name}, got ${exc}`; + } + + if (fullmsg === undefined) { + fullmsg = `Assertion failed: expected exception ${ctor.name}, no exception thrown`; + } + if (msg !== undefined) { + fullmsg += " - " + msg; + } + + throw new Error(fullmsg); +} + +function isWasmSupported() { + let testingFunctions = SpecialPowers.Cu.getJSTestingFunctions(); + return testingFunctions.wasmIsSupported(); +} + +function getWasmModule(_binary_) { + let module = new WebAssembly.Module(_binary_); + return module; +} + +function expectingSuccess(request) { + return new Promise(function (resolve, reject) { + request.onerror = function (event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onsuccess = function (event) { + resolve(event); + }; + request.onupgradeneeded = function (event) { + ok(false, "Got upgrade, but did not expect it!"); + reject(event); + }; + }); +} + +function expectingUpgrade(request) { + return new Promise(function (resolve, reject) { + request.onerror = function (event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onupgradeneeded = function (event) { + resolve(event); + }; + request.onsuccess = function (event) { + ok(false, "Got success, but did not expect it!"); + reject(event); + }; + }); +} + +function expectingError(request, errorName) { + return new Promise(function (resolve, reject) { + request.onerror = function (event) { + is(errorName, event.target.error.name, "Correct exception type"); + event.stopPropagation(); + resolve(event); + }; + request.onsuccess = function (event) { + ok(false, "Got success, but did not expect it!"); + reject(event); + }; + request.onupgradeneeded = function (event) { + ok(false, "Got upgrade, but did not expect it!"); + reject(event); + }; + }); +} + +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 () { + self.ok( + testSteps.constructor.name === "GeneratorFunction", + "Async/await tests shouldn't call finishTest()" + ); + if (self._expectingUncaughtException) { + self.ok( + false, + "expectUncaughtException was called but no uncaught " + + "exception was detected!" + ); + } + self.postMessage({ op: "done" }); + }; + + self.grabEventAndContinueHandler = function (_event_) { + testGenerator.next(_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(_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_); + }, + }; + + // TODO this is duplicate from the global compareKeys function defined above, + // this duplication should be avoided (bug 1565986) + 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; + } + + if (_k1_ instanceof ArrayBuffer) { + if (!(_k2_ instanceof ArrayBuffer)) { + return false; + } + + function arrayBuffersAreEqual(a, b) { + if (a.byteLength != b.byteLength) { + return false; + } + let ui8b = new Uint8Array(b); + return new Uint8Array(a).every((val, i) => val === ui8b[i]); + } + + return arrayBuffersAreEqual(_k1_, _k2_); + } + + 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 false; + } + ok( + false, + "Worker: uncaught exception [" + + _file_ + + ":" + + _line_ + + "]: '" + + _message_ + + "'" + ); + self.finishTest(); + self.close(); + return true; + }; + + self.isWasmSupported = function () { + return self.wasmSupported; + }; + + 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(async function () { + info("Worker: starting tests"); + if (testSteps.constructor.name === "AsyncFunction") { + await testSteps(); + if (self._expectingUncaughtException) { + self.ok( + false, + "expectUncaughtException was called but no " + + "uncaught exception was detected!" + ); + } + self.postMessage({ op: "done" }); + } else { + ok( + testSteps.constructor.name === "GeneratorFunction", + "Unsupported function type" + ); + testGenerator.next(); + } + }); + break; + + case "clearAllDatabasesDone": + info("Worker: all databases are cleared"); + if (self._clearAllDatabasesCallback) { + self._clearAllDatabasesCallback(); + } + break; + + default: + throw new Error( + "Received a bad message from parent: " + JSON.stringify(message) + ); + } + }; + + self.expectingSuccess = function (_request_) { + return new Promise(function (_resolve_, _reject_) { + _request_.onerror = function (_event_) { + ok(false, "indexedDB error, '" + _event_.target.error.name + "'"); + _reject_(_event_); + }; + _request_.onsuccess = function (_event_) { + _resolve_(_event_); + }; + _request_.onupgradeneeded = function (_event_) { + ok(false, "Got upgrade, but did not expect it!"); + _reject_(_event_); + }; + }); + }; + + self.expectingUpgrade = function (_request_) { + return new Promise(function (_resolve_, _reject_) { + _request_.onerror = function (_event_) { + ok(false, "indexedDB error, '" + _event_.target.error.name + "'"); + _reject_(_event_); + }; + _request_.onupgradeneeded = function (_event_) { + _resolve_(_event_); + }; + _request_.onsuccess = function (_event_) { + ok(false, "Got success, but did not expect it!"); + _reject_(_event_); + }; + }); + }; + + self.postMessage({ op: "ready" }); +} + +async function executeWorkerTestAndCleanUp(testScriptPath) { + info("Running test in a worker"); + + let workerScriptBlob = new Blob(["(" + workerScript.toString() + ")();"], { + type: "text/javascript", + }); + let workerScriptURL = URL.createObjectURL(workerScriptBlob); + + let worker; + try { + await new Promise(function (resolve, reject) { + 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(); + reject(); + }; + + worker.onmessage = function (event) { + let message = event.data; + switch (message.op) { + case "ok": + SimpleTest.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"); + resolve(); + break; + + case "expectUncaughtException": + worker._expectingUncaughtException = message.expecting; + break; + + case "clearAllDatabases": + clearAllDatabases(function () { + worker.postMessage({ op: "clearAllDatabasesDone" }); + }); + break; + + default: + ok( + false, + "Received a bad message from worker: " + JSON.stringify(message) + ); + reject(); + } + }; + }); + + URL.revokeObjectURL(workerScriptURL); + } catch (e) { + info("Unexpected thing happened: " + e); + } + + return new Promise(function (resolve) { + info("Cleaning up the databases"); + + if (worker._expectingUncaughtException) { + ok( + false, + "expectUncaughtException was called but no uncaught " + + "exception was detected!" + ); + } + + worker.terminate(); + worker = null; + + clearAllDatabases(resolve); + }); +} diff --git a/dom/indexedDB/test/leaving_page_iframe.html b/dom/indexedDB/test/leaving_page_iframe.html new file mode 100644 index 0000000000..4ed299df33 --- /dev/null +++ b/dom/indexedDB/test/leaving_page_iframe.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> + <script> +var db; +function startDBWork() { + indexedDB.open(parent.location, 1).onupgradeneeded = function(e) { + db = e.target.result; + 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/marionette/manifest.ini b/dom/indexedDB/test/marionette/manifest.ini new file mode 100644 index 0000000000..6b25253f10 --- /dev/null +++ b/dom/indexedDB/test/marionette/manifest.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_IDB_encryption_PBM.py] diff --git a/dom/indexedDB/test/marionette/test_IDB_encryption_PBM.py b/dom/indexedDB/test/marionette/test_IDB_encryption_PBM.py new file mode 100644 index 0000000000..c3d6a18ec2 --- /dev/null +++ b/dom/indexedDB/test/marionette/test_IDB_encryption_PBM.py @@ -0,0 +1,185 @@ +# 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/. + +import os +import re + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + +INDEXED_DB_PBM_PREF = "dom.indexedDB.privateBrowsing.enabled" +QM_TESTING_PREF = "dom.quotaManager.testing" + + +class IDBEncryptionPBM(MarionetteTestCase): + + """ + Bug1784966: Ensure IDB data gets encrypted in Private Browsing Mode. + We need to ensure data inside both sqlite fields and blob files under + *.sqllite gets encrypted. + """ + + def setUp(self): + super(IDBEncryptionPBM, self).setUp() + + self.IDBName = "IDBTest" + self.IDBStoreName = "IDBTestStore" + self.IDBVersion = 1 + self.IDBValue = "test_IDB_Encryption_PBM" + self.idbStoragePath = None + + self.profilePath = self.marionette.instance.profile.profile + + self.defaultIDBPrefValue = self.marionette.get_pref(INDEXED_DB_PBM_PREF) + self.marionette.set_pref(INDEXED_DB_PBM_PREF, True) + + self.defaultQMPrefValue = self.marionette.get_pref(QM_TESTING_PREF) + self.marionette.set_pref(QM_TESTING_PREF, True) + + # Navigate by opening a new private window + pbmWindowHandle = self.marionette.open(type="window", private=True)["handle"] + self.marionette.switch_to_window(pbmWindowHandle) + self.marionette.navigate( + self.marionette.absolute_url("dom/indexedDB/basicIDB_PBM.html") + ) + + def tearDown(self): + super(IDBEncryptionPBM, self).setUp() + + self.marionette.set_pref(INDEXED_DB_PBM_PREF, self.defaultIDBPrefValue) + self.marionette.set_pref(QM_TESTING_PREF, self.defaultQMPrefValue) + + # closes the new private window we opened in the setUp and referred by 'pbmWindowHandle' + self.marionette.close() + + def test_ensure_encrypted_blob(self): + self.marionette.execute_script( + """ + const [idb, store, key, value] = arguments; + const blobValue = new Blob([value], {type:'text/plain'}); + + window.wrappedJSObject.addDataIntoIDB(idb, store, key, blobValue); + """, + script_args=(self.IDBName, self.IDBStoreName, "blobKey", self.IDBValue), + ) + + self.ensureInvariantHolds(lambda _: self.sqliteWALReleased()) + self.ensureInvariantHolds( + lambda _: self.findDirObj(self.idbStoragePath, ".files", False) is not None + ) + + idbBlobDir = self.findDirObj(self.idbStoragePath, ".files", False) + + # seems like there's a timing issue here. There are sometimes no blob file + # even after WAL is released. Allowing some buffer time and ensuring blob file + # exists before validating it's contents + idbBlobPath = os.path.join(idbBlobDir, "1") + self.ensureInvariantHolds(lambda _: os.path.exists(idbBlobPath)) + + foundRawValue = False + with open(idbBlobPath, "rb") as f_binary: + foundRawValue = ( + re.search(self.IDBValue.encode("ascii"), f_binary.read()) is not None + ) + + self.assertFalse(foundRawValue, "Blob file did not get encrypted") + + def test_ensure_encrpted_sqlite_data(self): + self.marionette.execute_script( + """ + const [idb, store, key, value] = arguments; + window.wrappedJSObject.addDataIntoIDB(idb, store, key, value); + """, + script_args=(self.IDBName, self.IDBStoreName, "textKey", self.IDBValue), + ) + + self.ensureInvariantHolds(lambda _: self.sqliteWALReleased()) + self.ensureInvariantHolds( + lambda _: self.findDirObj(self.idbStoragePath, ".sqlite", True) is not None + ) + + sqliteDBFile = self.findDirObj(self.idbStoragePath, ".sqlite", True) + + foundRawValue = False + with open(sqliteDBFile, "rb") as f_binary: + foundRawValue = ( + re.search(self.IDBValue.encode("ascii"), f_binary.read()) is not None + ) + + self.assertFalse(foundRawValue, "sqlite data did not get encrypted") + + def getIDBStoragePath(self): + if self.idbStoragePath is not None: + return self.idbStoragePath + + fullOriginMetadata = self.getFullOriginMetadata( + self.marionette.absolute_url("")[:-1] + "^privateBrowsingId=1" + ) + + storageOrigin = fullOriginMetadata["storageOrigin"] + sanitizedStorageOrigin = storageOrigin.replace(":", "+").replace("/", "+") + + storagePath = os.path.join( + self.profilePath, "storage", "private", sanitizedStorageOrigin, "idb" + ) + + self.idbStoragePath = storagePath + + print("idb origin directory = " + self.idbStoragePath) + return self.idbStoragePath + + def getFullOriginMetadata(self, origin): + with self.marionette.using_context("chrome"): + res = self.marionette.execute_async_script( + """ + const [url, resolve] = arguments; + + function getOrigin() { + return new Promise((resolve, reject) => { + let context = "private" + let principal = Services.scriptSecurityManager. + createContentPrincipalFromOrigin(url); + + let qms = Services.qms; + let req = qms.getFullOriginMetadata(context, principal); + req.callback = () => { + if (req.resultCode != 0) reject("Error!"); + resolve(req.result); + }; + }); + } + + return getOrigin() + .then((result) => resolve(result)) + .catch((error) => resolve(null)); + """, + script_args=(origin,), + new_sandbox=False, + ) + + assert res is not None + return res + + def findDirObj(self, path, pattern, isFile): + for obj in os.scandir(path): + if obj.path.endswith(pattern) and (obj.is_file() == isFile): + return obj.path + return None + + def sqliteWALReleased(self): + """ + checks if .sqlite-wal has been cleared or not. + returns False if idbStoragePath does not exist + """ + if not os.path.exists(self.getIDBStoragePath()): + return False + walPath = self.findDirObj(self.idbStoragePath, ".sqlite-wal", True) + return walPath is None or os.stat(walPath).st_size == 0 + + def ensureInvariantHolds(self, op): + maxWaitTime = 60 + Wait(self.marionette, timeout=maxWaitTime).until( + op, + message=f"operation did not yield success even after waiting {maxWaitTime}s time", + ) diff --git a/dom/indexedDB/test/mochitest-common.ini b/dom/indexedDB/test/mochitest-common.ini new file mode 100644 index 0000000000..b0575b2bdd --- /dev/null +++ b/dom/indexedDB/test/mochitest-common.ini @@ -0,0 +1,273 @@ +# 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] +prefs = + extensions.blocklist.enabled=false +support-files = + bfcache_page1.html + bfcache_page2.html + blob_worker_crash_iframe.html + !/dom/events/test/event_leak_utils.js + error_events_abort_transactions_iframe.html + event_propagation_iframe.html + exceptions_in_events_iframe.html + file.js + helpers.js + leaving_page_iframe.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_constraint_error_messages.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_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_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_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_remove_objectStore.js + unit/test_rename_index.js + unit/test_rename_index_errors.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_upgrade_add_index.js + unit/test_unique_index_update.js + unit/test_view_put_get_values.js + unit/test_wasm_put_get_values.js + unit/test_writer_starvation.js + +[test_abort_deleted_index.html] +[test_abort_deleted_objectStore.html] +[test_abort_on_reload.html] +support-files = + abort_on_reload.html +skip-if = + http3 +[test_add_put.html] +[test_add_twice_failure.html] +[test_advance.html] +[test_autoIncrement.html] +[test_autoIncrement_indexes.html] +[test_bfcache.html] +skip-if = + http3 +[test_blob_file_backed.html] +[test_blob_simple.html] +[test_blob_worker_crash.html] +skip-if = os == 'win' && os_version == '6.1' # Bug 1342415 +[test_blob_worker_xhr_post.html] +skip-if = + (os == 'win' && os_version == '6.1') || (verify && (os == 'win')) # Bug 1342415 + http3 + socketprocess_networking # Bug 1827221: test is toggled off here rather than in private.ini + # because socketprocess_networking is an un-released feature so we + # do not loose much coverage by disabling it here. + +[test_blob_worker_xhr_post_multifile.html] +skip-if = + os == 'win' && os_version == '6.1' # Bug 1342415 + http3 + socketprocess_networking # Bug 1827221: test is toggled off here rather than in private.ini + # because socketprocess_networking is an un-released feature so we + # do not loose much coverage by disabling it here. + +[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_constraint_error_messages.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] +skip-if = verify +[test_event_listener_leaks.html] +skip-if = (os == "win" && processor == "aarch64") #bug 1535784 +[test_event_propagation.html] +skip-if = verify +[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_put_get_object.html] +[test_file_put_get_values.html] +[test_file_replace.html] +[test_file_resurrection_delete.html] +skip-if = toolkit == 'android' +[test_file_resurrection_transaction_abort.html] +skip-if = toolkit == 'android' +[test_file_sharing.html] +[test_file_transaction_abort.html] +[test_getAll.html] +[test_getFileId.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_maximal_serialized_object_size.html] +[test_message_manager_ipc.html] +[test_multientry.html] +[test_names_sorted.html] +skip-if = + (xorigin && !debug) # Hangs + os == "linux" && bits == 64 && !debug # Bug 1602927 +[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_put_get_values.html] +[test_put_get_values_autoIncrement.html] +[test_readonly_transactions.html] +[test_readwriteflush_disabled.html] +[test_remove_index.html] +[test_remove_objectStore.html] +[test_rename_index.html] +skip-if = os == "linux" && os_version == "18.04" #Bug 1601601 +[test_rename_index_errors.html] +[test_rename_objectStore.html] +[test_rename_objectStore_errors.html] +[test_request_readyState.html] +[test_sandbox.html] +skip-if = verify +[test_setVersion.html] +[test_setVersion_abort.html] +[test_setVersion_events.html] +[test_setVersion_exclusion.html] +[test_setVersion_throw.html] +[test_success_events_after_abort.html] +[test_table_locks.html] +[test_table_rollback.html] +[test_third_party.html] +support-files = + third_party_window.html + third_party_iframe1.html + third_party_iframe2.html +skip-if = + http3 +[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_upgrade_add_index.html] +skip-if = (!debug && bits == 64 && (os == "linux" || os == "mac")) || (os == "win") #Bug 1637715 +scheme = https +[test_view_put_get_values.html] +[test_wasm_put_get_values.html] +[test_writer_starvation.html] +skip-if = true #Bug 595368 diff --git a/dom/indexedDB/test/mochitest-intl-api.ini b/dom/indexedDB/test/mochitest-intl-api.ini new file mode 100644 index 0000000000..8ec4a172c4 --- /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-private.ini b/dom/indexedDB/test/mochitest-private.ini new file mode 100644 index 0000000000..2ff4a63858 --- /dev/null +++ b/dom/indexedDB/test/mochitest-private.ini @@ -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/. + +[DEFAULT] +dupe-manifest = +prefs = + browser.privatebrowsing.autostart=true + dom.indexedDB.privateBrowsing.enabled=true + extensions.blocklist.enabled=false +tags = indexedDB indexedDB-private + +[include:mochitest-common.ini] + +[test_file_os_delete.html] +skip-if = true # Bug 1819284: Run test_file_os_delete only for regular manifest. +[test_file_put_deleted.html] +skip-if = verify # Bug 1829690: Investigate failing test_file_put_delete.html in verify mode for PBM. +[test_storage_manager_estimate.html] +scheme = https +skip-if = xorigin diff --git a/dom/indexedDB/test/mochitest-regular.ini b/dom/indexedDB/test/mochitest-regular.ini new file mode 100644 index 0000000000..a6134f0060 --- /dev/null +++ b/dom/indexedDB/test/mochitest-regular.ini @@ -0,0 +1,17 @@ +# 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 = +prefs = + extensions.blocklist.enabled=false +tags = indexedDB indexedDB-regular + +[include:mochitest-common.ini] + +[test_file_os_delete.html] # Bug 1819284: Run test_file_os_delete only for regular manifest. +skip-if = xorigin # Bug 1827617: Investigate test_file_os_delete.html failure in xorigin. +[test_file_put_deleted.html] # Bug 1829690: Investigate failing test_file_put_delete.html in verify mode for PBM. +[test_storage_manager_estimate.html] +scheme = https diff --git a/dom/indexedDB/test/page_private_idb.html b/dom/indexedDB/test/page_private_idb.html new file mode 100644 index 0000000000..795a814981 --- /dev/null +++ b/dom/indexedDB/test/page_private_idb.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + All the interesting stuff happens in ContentTask.spawn() calls. +</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 0000000000..1b888869b7 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_abort_deleted_index.js"></script> + <script type="text/javascript" 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 0000000000..cc3c8a6432 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_abort_deleted_objectStore.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_abort_on_reload.html b/dom/indexedDB/test/test_abort_on_reload.html new file mode 100644 index 0000000000..fd31e709c6 --- /dev/null +++ b/dom/indexedDB/test/test_abort_on_reload.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + let openedWindow + let reloads = 0 + + function openWindow() { + openedWindow = window.open("abort_on_reload.html"); + } + + function messageListener(event) { + ok(true, "reload recorded"); + + if (++reloads == 20) { + openedWindow.close(); + SimpleTest.finish(); + } + } + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + window.addEventListener("message", messageListener); + + openWindow(); + } + </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 0000000000..f5fe8001c2 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_add_put.js"></script> + <script type="text/javascript" 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 0000000000..dd5b3ee55f --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_add_twice_failure.js"></script> + <script type="text/javascript" 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 0000000000..db04bfc071 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_advance.js"></script> + <script type="text/javascript" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_autoIncrement.html b/dom/indexedDB/test/test_autoIncrement.html new file mode 100644 index 0000000000..f7658731e6 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_autoIncrement.js"></script> + <script type="text/javascript" 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 0000000000..b018cfb7a5 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_autoIncrement_indexes.js"></script> + <script type="text/javascript" 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 0000000000..bc685e2801 --- /dev/null +++ b/dom/indexedDB/test/test_bfcache.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 Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + function* testSteps() + { + window.onmessage = grabEventAndContinueHandler; + + let testWin = window.open("bfcache_page1.html", "testWin"); + var event = yield undefined; + is(event.data, "go", "set up database successfully"); + + testWin.location = "bfcache_page2.html"; + let 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"); + + testWin.close(); + finishTest(); + } + </script> + <script type="text/javascript" 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 0000000000..6fe4775804 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_blob_file_backed.js"></script> + <script type="text/javascript" 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 0000000000..7dc2fee767 --- /dev/null +++ b/dom/indexedDB/test/test_blob_simple.html @@ -0,0 +1,280 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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, 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() { + /* eslint-env worker */ + 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.toString(), ")()"])); + + 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(); + } + </script> + <script type="text/javascript" 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 0000000000..8d242446a9 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + /* import-globals-from helpers.js */ + /* + * 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); + // 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.remove(); + iframe = null; + scheduleGC(); + yield undefined; + + info("If we are still alive, then we win!"); + ok("Did not crash / trigger an assert!"); + + finishTest(); + } + </script> + <script type="text/javascript" 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 0000000000..5c53e080f5 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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() { + /* eslint-env worker */ + 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.toString(), ")()"])); + + 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(); + } + </script> + <script type="text/javascript" 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 0000000000..a2861f1e5c --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + /** + * 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() { + /* eslint-env worker */ + 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.toString(), ")()"])); + + 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(); + } + </script> + <script type="text/javascript" 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 0000000000..9a438d4f8a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + /** + * 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() { + /* eslint-env worker */ + 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.toString(), ")()"])); + + 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(); + } + </script> + <script type="text/javascript" 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 0000000000..7a6eaeaed6 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + /** + * 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() { + /* eslint-env worker */ + 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.toString(), ")()"])); + + 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(); + } + </script> + <script type="text/javascript" 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 0000000000..2606df4c0a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_blocked_order.js"></script> + <script type="text/javascript" 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 0000000000..5a6a9c283f --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_lifetimes.js"></script> + <script type="text/javascript" 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 0000000000..f8608bbde2 --- /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 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"> + + 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 0000000000..002add6cb4 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_clear.js"></script> + <script type="text/javascript" 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 0000000000..1b5086be0d --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_complex_keyPaths.js"></script> + <script type="text/javascript" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_constraint_error_messages.html b/dom/indexedDB/test/test_constraint_error_messages.html new file mode 100644 index 0000000000..a7aceaf247 --- /dev/null +++ b/dom/indexedDB/test/test_constraint_error_messages.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_constraint_error_messages.js"></script> + <script type="text/javascript" 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 0000000000..d85494be2b --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_count.js"></script> + <script type="text/javascript" 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 0000000000..af96a63739 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_create_index.js"></script> + <script type="text/javascript" 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 0000000000..fc414404c2 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_create_index_with_integer_keys.js"></script> + <script type="text/javascript" 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 0000000000..d7b80cec9c --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_create_locale_aware_index.js"></script> + <script type="text/javascript" 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 0000000000..f7311370a9 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_create_objectStore.js"></script> + <script type="text/javascript" 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 0000000000..f588188c08 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_cursor_mutation.js"></script> + <script type="text/javascript" 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 0000000000..1c6f0d4cf8 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_cursor_update_updates_indexes.js"></script> + <script type="text/javascript" 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 0000000000..548cab315c --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_cursors.js"></script> + <script type="text/javascript" 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 0000000000..b6f4f50f10 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_database_onclose.js"></script> + <script type="text/javascript" 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 0000000000..ac255c9cc2 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_deleteDatabase.js"></script> + <script type="text/javascript" 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 0000000000..4385e936af --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_deleteDatabase_interactions.js"></script> + <script type="text/javascript" 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 0000000000..9bb54f2edc --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_deleteDatabase_onblocked.js"></script> + <script type="text/javascript" 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 0000000000..161db95ed1 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_deleteDatabase_onblocked_duringVersionChange.js"></script> + <script type="text/javascript" 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 0000000000..32a8271aed --- /dev/null +++ b/dom/indexedDB/test/test_error_events_abort_transactions.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function messageListener(event) { + // eslint-disable-next-line no-eval + eval(event.data); + } + + window.addEventListener("message", messageListener); + } + </script> + +</head> + +<body onload="runTest();"> + <iframe src="error_events_abort_transactions_iframe.html"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/test_event_listener_leaks.html b/dom/indexedDB/test/test_event_listener_leaks.html new file mode 100644 index 0000000000..d1d9a69068 --- /dev/null +++ b/dom/indexedDB/test/test_event_listener_leaks.html @@ -0,0 +1,61 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450358 - Test IDB event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate IDB objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +let count = 0; +async function useIDB(contentWindow) { + count += 1; + let db = await new Promise(resolve => { + let r = contentWindow.indexedDB.open("idb-leak-test-" + count, 1.0); + r.onupgradeneeded = evt => { + evt.target.result.createObjectStore("looped"); + }; + r.onsuccess = evt => { + resolve(evt.target.result); + }; + }); + + let tx = db.transaction("looped", "readwrite"); + let store = tx.objectStore("looped"); + + function spin() { + contentWindow.spinCount += 1; + store.get(0).onsuccess = spin; + } + + store.put(0, "purgatory").onsuccess = e => { + contentWindow.putCount += 1; + spin(); + }; +} + +async function runTest() { + try { + await checkForEventListenerLeaks("IDB", useIDB); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</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 0000000000..192431c1b8 --- /dev/null +++ b/dom/indexedDB/test/test_event_propagation.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function messageListener(event) { + // eslint-disable-next-line no-eval + eval(event.data); + } + + window.addEventListener("message", messageListener); + } + </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 0000000000..c4318bbb7a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_event_source.js"></script> + <script type="text/javascript" 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 0000000000..34e30ea8a9 --- /dev/null +++ b/dom/indexedDB/test/test_exceptions_in_events.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + function runTest() { + SimpleTest.waitForExplicitFinish(); + + function messageListener(event) { + // eslint-disable-next-line no-eval + eval(event.data); + } + + window.addEventListener("message", messageListener); + } + </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 0000000000..fdd1e1b58d --- /dev/null +++ b/dom/indexedDB/test/test_file_array.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..8a22a188e8 --- /dev/null +++ b/dom/indexedDB/test/test_file_cross_database_copying.html @@ -0,0 +1,107 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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; + let 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); + + let request = objectStore.add(refResult, 2); + request.onsuccess = grabEventAndContinueHandler; + let 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(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..4eefffebf6 --- /dev/null +++ b/dom/indexedDB/test/test_file_delete.html @@ -0,0 +1,131 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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; + } + + yield* assertEventuallyHasNoFileInfo(name, 1); + yield* assertEventuallyHasFileInfo(name, 2); + yield* assertEventuallyHasFileInfo(name, 3); + + { + 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); + let 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"); + + yield *assertEventuallyWithGC(() => -1 == getFileDBRefCount(name, 3), + "Correct db ref count"); + + event = null; + result = null; + } + + yield* assertEventuallyHasNoFileInfo(name, 1); + yield* assertEventuallyHasNoFileInfo(name, 2); + yield* assertEventuallyHasNoFileInfo(name, 3); + + finishTest(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..547fc415e3 --- /dev/null +++ b/dom/indexedDB/test/test_file_os_delete.html @@ -0,0 +1,111 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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 originalUsage = yield undefined; + + // FIXME This might not work in private browsing mode because of the + // encryption overhead. We need to decide how to handle that. We might + // change the usage reporting to remove the overhead. + + is(originalUsage, 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); + let usage = yield undefined; + + is(usage, originalUsage, "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(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..32e197b1da --- /dev/null +++ b/dom/indexedDB/test/test_file_put_deleted.html @@ -0,0 +1,155 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + /** + * 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; + let 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|private)[/\\]/) + 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); + + let 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"); + yield undefined; + yield undefined; + 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. + let request = objectStore.getKey(2); + request.onsuccess = grabEventAndContinueHandler; + request.onerror = errorHandler; + let event = yield undefined; + + let result = event.target.result; + is(result, undefined, "no key found"); // (the get returns undefined) + } + + finishTest(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..9fea94a7a4 --- /dev/null +++ b/dom/indexedDB/test/test_file_put_get_object.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + function* testSteps() + { + 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(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..048a140ff1 --- /dev/null +++ b/dom/indexedDB/test/test_file_put_get_values.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..340d8597c9 --- /dev/null +++ b/dom/indexedDB/test/test_file_replace.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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, refs, dbRefs); + ok(hasFileInfo, `Expect existing DatabaseFileInfo for ${name}/${id}`); + is(refs.value, 1, "Correct ref count"); + is(dbRefs.value, id / 100 >> 0, "Correct db ref count"); + } + + finishTest(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..1d128a8791 --- /dev/null +++ b/dom/indexedDB/test/test_file_resurrection_delete.html @@ -0,0 +1,126 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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; + } + + yield* assertEventuallyFileRefCount(name, 1, 0); + + { + 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); + let 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; + } + + yield* assertEventuallyFileRefCount(name, 1, 0); + + finishTest(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..e79df40f3b --- /dev/null +++ b/dom/indexedDB/test/test_file_resurrection_transaction_abort.html @@ -0,0 +1,88 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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; + } + + yield* assertEventuallyFileRefCount(name, 1, 0); + + finishTest(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..c62972be0e --- /dev/null +++ b/dom/indexedDB/test/test_file_sharing.html @@ -0,0 +1,102 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..e2429e1f32 --- /dev/null +++ b/dom/indexedDB/test/test_file_transaction_abort.html @@ -0,0 +1,73 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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; + } + + yield* assertEventuallyHasNoFileInfo(name, 1); + + finishTest(); + } + </script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..954a83c46d --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_getAll.js"></script> + <script type="text/javascript" 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 0000000000..2ae4e290ba --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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" src="file.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_globalObjects_chrome.xhtml b/dom/indexedDB/test/test_globalObjects_chrome.xhtml new file mode 100644 index 0000000000..7471ffffd1 --- /dev/null +++ b/dom/indexedDB/test/test_globalObjects_chrome.xhtml @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/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 src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from chromeHelpers.js */ + 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" 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 0000000000..2358fdc619 --- /dev/null +++ b/dom/indexedDB/test/test_globalObjects_content.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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(); + } + </script> + <script type="text/javascript" src="helpers.js"></script> +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_global_data.html b/dom/indexedDB/test/test_global_data.html new file mode 100644 index 0000000000..954519ffcf --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_global_data.js"></script> + <script type="text/javascript" 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 0000000000..532abdd383 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_index_empty_keyPath.js"></script> + <script type="text/javascript" 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 0000000000..03a036503a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_index_getAll.js"></script> + <script type="text/javascript" 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 0000000000..cdf3a55d06 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_index_getAllObjects.js"></script> + <script type="text/javascript" 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 0000000000..bbf55b5f54 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_index_object_cursors.js"></script> + <script type="text/javascript" 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 0000000000..fce805cf6a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_index_update_delete.js"></script> + <script type="text/javascript" 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 0000000000..4f0e99ce4e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_indexes.js"></script> + <script type="text/javascript" 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 0000000000..bbca247837 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_indexes_bad_values.js"></script> + <script type="text/javascript" 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 0000000000..b9be3896c9 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_indexes_funny_things.js"></script> + <script type="text/javascript" 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 0000000000..0afc577e9f --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_invalid_cursor.js"></script> + <script type="text/javascript" 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 0000000000..cf119249e2 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_invalid_version.js"></script> + <script type="text/javascript" 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 0000000000..31092fa7b3 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_invalidate.js"></script> + <script type="text/javascript" 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 0000000000..a802108230 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_key_requirements.js"></script> + <script type="text/javascript" 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 0000000000..fe183f7731 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_keys.js"></script> + <script type="text/javascript" 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 0000000000..d33e53150a --- /dev/null +++ b/dom/indexedDB/test/test_leaving_page.html @@ -0,0 +1,48 @@ +<!-- + 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 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"> + window.addEventListener("message", 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(); + } + </script> + <script type="text/javascript" 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 0000000000..0623cac96e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_locale_aware_index_getAll.js"></script> + <script type="text/javascript" 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 0000000000..2d6bc16618 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_locale_aware_index_getAllObjects.js"></script> + <script type="text/javascript" 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 0000000000..02d420aa24 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_locale_aware_indexes.js"></script> + <script type="text/javascript" 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 0000000000..f08c5d20c3 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_maximal_serialized_object_size.js"></script> + <script type="text/javascript" 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 0000000000..39ef8f2d11 --- /dev/null +++ b/dom/indexedDB/test/test_message_manager_ipc.html @@ -0,0 +1,323 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for sending IndexedDB Blobs through MessageManager</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="application/javascript"> +"use strict"; + +async function chromeScriptFunc() { + function childFrameScript() { + /* eslint-env mozilla/frame-script */ + "use strict"; + + 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 }); + } + + function ok(condition, name, diag) { + sendAsyncMessage(mmName, + { op: "ok", + condition, + name, + 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 }); + } + + function grabAndContinue(arg) { + testGenerator.next(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.isInstance(blob), "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(Blob.isInstance(slice), "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.isInstance(blob), "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(Blob.isInstance(slice), "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]); + } + + let testGenerator = testSteps(); + testGenerator.next(); + } + + /* globals Services, createWindowlessBrowser */ + let { windowlessBrowser, browser } = await createWindowlessBrowser(); + + const system = Services.scriptSecurityManager.getSystemPrincipal(); + browser.loadURI( + Services.io.newURI("data:text/html,<!DOCTYPE HTML><html><body></body></html>"), + { triggeringPrincipal: system } + ); + + function finish() { + sendAsyncMessage("done"); + windowlessBrowser.close(); + } + + let mm = browser.messageManager; + + 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.next(arg); + } + + function* testSteps() { + let result = yield undefined; + + is(Cu.getClassName(result, true), "Array", "Child delivered an array of results"); + is(result.length, 2, "Child delivered two results"); + + let blob = result[0]; + is(Blob.isInstance(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(Blob.isInstance(slice), 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(Blob.isInstance(slice), true, "Child delivered a slice"); + 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"); + + finish(); + } + + 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.next(data.result); + break; + } + + default: { + ok(false, "Unknown op: " + data.op); + finish(); + } + } + }); + + mm.loadFrameScript(`data:,(${childFrameScript})();`, + false); + + mm.sendAsyncMessage(messageName, blob); +} + +add_task(async function() { + let chromeScript = SpecialPowers.loadChromeScript(chromeScriptFunc); + await chromeScript.promiseOneMessage("done"); + await chromeScript.destroy(); +}); + </script> + </body> +</html> diff --git a/dom/indexedDB/test/test_multientry.html b/dom/indexedDB/test/test_multientry.html new file mode 100644 index 0000000000..46bfe1628c --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_multientry.js"></script> + <script type="text/javascript" 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 0000000000..c2aa4dfbf7 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_names_sorted.js"></script> + <script type="text/javascript" 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 0000000000..3a5d5e7582 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_objectCursors.js"></script> + <script type="text/javascript" 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 0000000000..351113c7ba --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_objectStore_getAllKeys.js"></script> + <script type="text/javascript" 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 0000000000..0237b45d1a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_objectStore_inline_autoincrement_key_added_on_put.js"></script> + <script type="text/javascript" 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 0000000000..47c02ad4fe --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_objectStore_openKeyCursor.js"></script> + <script type="text/javascript" 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 0000000000..2efdadc101 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_objectStore_remove_values.js"></script> + <script type="text/javascript" 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 0000000000..bf4bba4d5e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_object_identity.js"></script> + <script type="text/javascript" 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 0000000000..e467e14977 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_odd_result_order.js"></script> + <script type="text/javascript" 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 0000000000..086d72f987 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_open_empty_db.js"></script> + <script type="text/javascript" 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 0000000000..bd9da12f74 --- /dev/null +++ b/dom/indexedDB/test/test_open_for_principal.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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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(); + } + </script> + <script type="text/javascript" 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 0000000000..dfcc8a6681 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_open_objectStore.js"></script> + <script type="text/javascript" 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 0000000000..153779482b --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_optionalArguments.js"></script> + <script type="text/javascript" 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 0000000000..8e9f4bf068 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_overlapping_transactions.js"></script> + <script type="text/javascript" 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 0000000000..2b7b313f1c --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_put_get_values.js"></script> + <script type="text/javascript" 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 0000000000..66f1578f00 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_put_get_values_autoIncrement.js"></script> + <script type="text/javascript" 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 0000000000..9af05ef72e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_readonly_transactions.js"></script> + <script type="text/javascript" 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 0000000000..d800ffce00 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_readwriteflush_disabled.js"></script> + <script type="text/javascript" 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 0000000000..2a96c1c513 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_remove_index.js"></script> + <script type="text/javascript" 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 0000000000..533219bb17 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_remove_objectStore.js"></script> + <script type="text/javascript" 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 0000000000..ccc2674765 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_rename_index.js"></script> + <script type="text/javascript" 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 0000000000..431e7fa95e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_rename_index_errors.js"></script> + <script type="text/javascript" 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 0000000000..982db9e48b --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_rename_objectStore.js"></script> + <script type="text/javascript" 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 0000000000..91fd837588 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_rename_objectStore_errors.js"></script> + <script type="text/javascript" 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 0000000000..9169cc0a18 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_request_readyState.js"></script> + <script type="text/javascript" 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 0000000000..0a15a53287 --- /dev/null +++ b/dom/indexedDB/test/test_sandbox.html @@ -0,0 +1,103 @@ +<!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; + // Step-done is defined in the sandbox and used in the add() function above. + /* global step_done */ + var testPromise = new Promise(r => resolvePromise = r); + SpecialPowers.Cu.exportFunction(_ => resolvePromise(), sandbox, + { defineAs: "step_done" }); + SpecialPowers.Cu.evalInSandbox("(" + testFunc.toString() + ")()" + + ".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_setVersion.html b/dom/indexedDB/test/test_setVersion.html new file mode 100644 index 0000000000..7627b3021d --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_setVersion.js"></script> + <script type="text/javascript" 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 0000000000..15f432a89b --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_setVersion_abort.js"></script> + <script type="text/javascript" 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 0000000000..d199bb6e02 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_setVersion_events.js"></script> + <script type="text/javascript" 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 0000000000..e30ca4b09f --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_setVersion_exclusion.js"></script> + <script type="text/javascript" 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 0000000000..312468921e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_setVersion_throw.js"></script> + <script type="text/javascript" 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 0000000000..b946f8c6e1 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_storage_manager_estimate.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="setup(isXOrigin);"></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 0000000000..1f4360032a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_success_events_after_abort.js"></script> + <script type="text/javascript" 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 0000000000..9dcad11fdf --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_table_locks.js"></script> + <script type="text/javascript" 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 0000000000..73b26f8329 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_table_rollback.js"></script> + <script type="text/javascript" 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 0000000000..ee90de8dac --- /dev/null +++ b/dom/indexedDB/test/test_third_party.html @@ -0,0 +1,119 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript"> + 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, expectedResultFrame1: true, expectedResultFrame2: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_ACCEPT, expectedResultFrame1: true, expectedResultFrame2: true }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_ACCEPT, expectedResultFrame1: true, expectedResultFrame2: true }, + + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECT, expectedResultFrame1: false, expectedResultFrame2: false }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_REJECT, expectedResultFrame1: false, expectedResultFrame2: false }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECT, expectedResultFrame1: false, expectedResultFrame2: false }, + + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: true, expectedResultFrame2: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, + + { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: true, expectedResultFrame2: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: false, expectedResultFrame2: 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 openedWindow; + + // Cookie preference changes are only applied to top-level tabs/windows + // when they are loaded. We need a window-proxy to continue the test. + function openWindow() { + SpecialPowers.pushPrefEnv({ + "set": [ + ["network.cookie.cookieBehavior", testData[testIndex].cookieBehavior], + ], + }, () => { + openedWindow = window.open("third_party_window.html"); + openedWindow.onload = _ => { + openedWindow.postMessage({ + source: "parent", + href: iframe2URL, + iframeUrl: testData[testIndex].host + iframe1Path, + }, "*"); + }; + }); + } + + let testFrames = ["iframe1", "iframe2"]; + function messageListener(event) { + let message = JSON.parse(event.data); + + // TODO: This is an ad-hoc solution to get a useful assertion message. + // It would be desirable that the test framework provides the ability + // to capture context information and provide it on assertion failures, + // automatically stringified. + let testContext = `testData[${testIndex}] == ${JSON.stringify(testData[testIndex])}`; + + let idx = testFrames.indexOf(message.source); + if (idx != -1) { + testFrames.splice(idx, 1); + if (message.source == "iframe1") { + is(message.result, testData[testIndex].expectedResultFrame1, `Good result for ${testContext} iframe1`); + } else if (message.source == "iframe2") { + is(message.result, testData[testIndex].expectedResultFrame2, `Good result for ${testContext} iframe2`); + } + } else { + ok(false, 'Test has already received a message from ${message.source}'); + } + + if (testFrames.length) { + return; + } + + openedWindow.close(); + + if (testIndex < testData.length - 1) { + testFrames = ["iframe1", "iframe2"]; + testIndex++; + openWindow(); + return; + } + + SimpleTest.finish(); + } + + function runTest() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.addPermission("indexedDB", true, document); + + window.addEventListener("message", messageListener); + openWindow(); + } + </script> + +</head> + +<body onload="runTest();"> +</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 0000000000..9ec23357af --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_traffic_jam.js"></script> + <script type="text/javascript" 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 0000000000..79fc37eb0e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_abort.js"></script> + <script type="text/javascript" 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 0000000000..5afb442f65 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_abort_hang.js"></script> + <script type="text/javascript" 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 0000000000..6bc1fffbe9 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_duplicate_store_names.js"></script> + <script type="text/javascript" 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 0000000000..c938f0f877 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_error.js"></script> + <script type="text/javascript" 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 0000000000..061f15c7b7 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_lifetimes.js"></script> + <script type="text/javascript" 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 0000000000..a38263cb6d --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_lifetimes_nested.js"></script> + <script type="text/javascript" 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 0000000000..5155ad237e --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_transaction_ordering.js"></script> + <script type="text/javascript" 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 0000000000..b1c91c5916 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_unique_index_update.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_upgrade_add_index.html b/dom/indexedDB/test/test_upgrade_add_index.html new file mode 100644 index 0000000000..d59ef48575 --- /dev/null +++ b/dom/indexedDB/test/test_upgrade_add_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 Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_upgrade_add_index.js"></script> + <script type="text/javascript" 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 0000000000..8d55ac0f2a --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_view_put_get_values.js"></script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" 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 0000000000..f7444436d9 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_wasm_put_get_values.js"></script> + <script type="text/javascript" src="file.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_wrappedArray.xhtml b/dom/indexedDB/test/test_wrappedArray.xhtml new file mode 100644 index 0000000000..7352852fb3 --- /dev/null +++ b/dom/indexedDB/test/test_wrappedArray.xhtml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1509168 +--> +<window title="Mozilla Bug 1509168" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1509168" + target="_blank">Mozilla Bug 1509168</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + function go() { + window.iwin = document.getElementById('ifr').contentWindow; + + let evilarr = new window.iwin.Uint8ClampedArray(3); + + try { + window.indexedDB.cmp(evilarr, undefined); + } catch (e) {} + + ok(true, "should reach here instead of crash"); + } + ]]> + </script> + <iframe id="ifr" onload="go();" /> +</window> diff --git a/dom/indexedDB/test/test_writer_starvation.html b/dom/indexedDB/test/test_writer_starvation.html new file mode 100644 index 0000000000..0273f73548 --- /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 src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_writer_starvation.js"></script> + <script type="text/javascript" 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 0000000000..d0db9a2602 --- /dev/null +++ b/dom/indexedDB/test/third_party_iframe1.html @@ -0,0 +1,48 @@ +<!-- + 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 messageListener(event) { + let message = JSON.parse(event.data); + + if (message.source == "parent") { + document.getElementById("iframe2").src = message.href; + } + else if (message.source == "iframe2") { + parent.postMessage(event.data, "*"); + } + } + + function report(result) { + let message = { source: "iframe1" }; + message.result = result; + window.parent.postMessage(JSON.stringify(message), "*"); + } + + function runIndexedDBTest() { + window.addEventListener('message', messageListener); + + 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();"> + <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 0000000000..da60c36bcf --- /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"> + function report(result) { + let message = { source: "iframe2" }; + message.result = result; + window.parent.postMessage(JSON.stringify(message), "*"); + } + + 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/third_party_window.html b/dom/indexedDB/test/third_party_window.html new file mode 100644 index 0000000000..14b16bf5d4 --- /dev/null +++ b/dom/indexedDB/test/third_party_window.html @@ -0,0 +1,33 @@ +<!-- + 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"> + let init = false; + onmessage = evt => { + if (!init) { + init = true; + + let iframe = document.getElementById("iframe1"); + iframe.src = evt.data.iframeUrl; + + iframe.addEventListener("load", e => { + iframe.contentWindow.postMessage(JSON.stringify(evt.data), "*"); + }); + + return; + } + + opener.postMessage(evt.data, "*"); + }; + </script> +</head> + +<body> + <iframe id="iframe1"></iframe> +</body> + +</html> diff --git a/dom/indexedDB/test/unit/GlobalObjectsChild.js b/dom/indexedDB/test/unit/GlobalObjectsChild.js new file mode 100644 index 0000000000..898f78128c --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsChild.js @@ -0,0 +1,39 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from xpcshell-head-parent-process.js */ + +function ok(cond, msg) { + dump("ok(" + cond + ', "' + msg + '")'); + Assert.ok(!!cond, Components.stack.caller); +} + +function finishTest() { + executeSoon(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/GlobalObjectsModule.sys.mjs b/dom/indexedDB/test/unit/GlobalObjectsModule.sys.mjs new file mode 100644 index 0000000000..c8bb420b3d --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsModule.sys.mjs @@ -0,0 +1,29 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +export function GlobalObjectsModule() {} + +GlobalObjectsModule.prototype = { + runTest() { + 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 0000000000..480746f5d0 --- /dev/null +++ b/dom/indexedDB/test/unit/GlobalObjectsSandbox.js @@ -0,0 +1,24 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from xpcshell-head-parent-process.js */ + +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 0000000000..db3cfe6246 --- /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 0000000000..2dcc10ffe0 --- /dev/null +++ b/dom/indexedDB/test/unit/defaultStorageUpgrade_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 0000000000..77f26035bc --- /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 0000000000..faa95ee843 --- /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 0000000000..e5302b36cf --- /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 0000000000..a01d49166e --- /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 0000000000..4c89acf0ae --- /dev/null +++ b/dom/indexedDB/test/unit/mutableFileUpgrade_profile.zip diff --git a/dom/indexedDB/test/unit/obsoleteOriginAttributes_profile.zip b/dom/indexedDB/test/unit/obsoleteOriginAttributes_profile.zip Binary files differnew file mode 100644 index 0000000000..01b9cfe118 --- /dev/null +++ b/dom/indexedDB/test/unit/obsoleteOriginAttributes_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 0000000000..09209d351a --- /dev/null +++ b/dom/indexedDB/test/unit/oldDirectories_profile.zip diff --git a/dom/indexedDB/test/unit/orphaned_files_profile.zip b/dom/indexedDB/test/unit/orphaned_files_profile.zip Binary files differnew file mode 100644 index 0000000000..0256dcfcf9 --- /dev/null +++ b/dom/indexedDB/test/unit/orphaned_files_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 0000000000..e13cce9d2e --- /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 0000000000..d08f88ea52 --- /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 0000000000..114d72e6cf --- /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 0000000000..f9635fc9f5 --- /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 0000000000..74c84ecbfe --- /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 0000000000..28bf9cbd5c --- /dev/null +++ b/dom/indexedDB/test/unit/test_abort_deleted_index.js @@ -0,0 +1,95 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..88de818e5c --- /dev/null +++ b/dom/indexedDB/test/unit/test_abort_deleted_objectStore.js @@ -0,0 +1,79 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..ba880957c0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_add_put.js @@ -0,0 +1,190 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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; + + 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 || 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, + }); + + let test = + " for test " + + JSON.stringify({ + autoincrement, + keypath, + method, + explicit: explicit === undefined ? "undefined" : explicit, + 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 + let 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(); +} 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 0000000000..cbc921aa87 --- /dev/null +++ b/dom/indexedDB/test/unit/test_add_twice_failure.js @@ -0,0 +1,41 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_advance.js b/dom/indexedDB/test/unit/test_advance.js new file mode 100644 index 0000000000..eb18c68af4 --- /dev/null +++ b/dom/indexedDB/test/unit/test_advance.js @@ -0,0 +1,181 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_autoIncrement.js b/dom/indexedDB/test/unit/test_autoIncrement.js new file mode 100644 index 0000000000..6f502deaf0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_autoIncrement.js @@ -0,0 +1,588 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator, disableWorkerTest */ +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); + let 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 } + ); + 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 } + ); + 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(); +} 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 0000000000..64de7f4efb --- /dev/null +++ b/dom/indexedDB/test/unit/test_autoIncrement_indexes.js @@ -0,0 +1,60 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_bad_origin_directory.js b/dom/indexedDB/test/unit/test_bad_origin_directory.js new file mode 100644 index 0000000000..07968c4b3b --- /dev/null +++ b/dom/indexedDB/test/unit/test_bad_origin_directory.js @@ -0,0 +1,29 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + const url = "ftp://ftp.example.com"; + const name = "test_bad_origin_directory.js"; + + let uri = Services.io.newURI(url); + + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + info("Opening database"); + + let request = indexedDB.openForPrincipal(principal, name); + request.onerror = continueToNextStepSync; + request.onsuccess = unexpectedSuccessHandler; + yield undefined; + + 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 0000000000..ba973d63d5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_blob_file_backed.js @@ -0,0 +1,79 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator, disableWorkerTest */ +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(); +} 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 0000000000..212b1b3254 --- /dev/null +++ b/dom/indexedDB/test/unit/test_blocked_order.js @@ -0,0 +1,177 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_bug1056939.js b/dom/indexedDB/test/unit/test_bug1056939.js new file mode 100644 index 0000000000..531a5df94d --- /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/ + */ + +/* exported testGenerator */ +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 0000000000..cac3c9bf60 --- /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/ + */ + +/* exported testGenerator, disableWorkerTest */ +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.next(true); + }; + trans.onabort = function (event) { + is(trans.error.name, "QuotaExceededError", "Reached quota limit"); + testGenerator.next(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 0000000000..d504ff8851 --- /dev/null +++ b/dom/indexedDB/test/unit/test_clear.js @@ -0,0 +1,90 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..87fd191d8f --- /dev/null +++ b/dom/indexedDB/test/unit/test_complex_keyPaths.js @@ -0,0 +1,329 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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( + // eslint-disable-next-line no-self-compare + 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 { + var 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) { + let splitDestProp = destProp.split("."); + if (splitDestProp.length == 1) { + newValue[splitDestProp[0]] = "newKeyValue"; + } else if (splitDestProp.length == 2) { + newValue[splitDestProp[0]][splitDestProp[1]] = "newKeyValue"; + } else { + newValue[splitDestProp[0]][splitDestProp[1]][splitDestProp[2]] = + "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( + // eslint-disable-next-line no-self-compare + 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; + let e = yield undefined; + is(e.target.result, 1, "found value when reading" + test); + } else { + index.count().onsuccess = grabEventAndContinueHandler; + let 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(); +} diff --git a/dom/indexedDB/test/unit/test_constraint_error_messages.js b/dom/indexedDB/test/unit/test_constraint_error_messages.js new file mode 100644 index 0000000000..b77431e71a --- /dev/null +++ b/dom/indexedDB/test/unit/test_constraint_error_messages.js @@ -0,0 +1,64 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testSteps */ +async function testSteps() { + const name = this.window ? window.location.pathname : "Splendid Test"; + const objectStoreName = "foo"; + const indexName = "bar", + keyPath = "bar"; + + info("Opening database"); + + let request = indexedDB.open(name); + let event = await expectingUpgrade(request); + + let db = event.target.result; + + info("Creating objectStore"); + + let objectStore = db.createObjectStore(objectStoreName); + + info("Creating a duplicated object store to get an error"); + + try { + db.createObjectStore(objectStoreName); + ok( + false, + "ConstraintError should be thrown if object store already exists" + ); + } catch (e) { + ok(true, "ConstraintError should be thrown if object store already exists"); + is( + e.message, + "IDBDatabase.createObjectStore: Object store named '" + + objectStoreName + + "' already exists at index '0'", + "Threw with correct error message" + ); + } + + info("Creating an index"); + + objectStore.createIndex(indexName, keyPath); + + info("Creating a duplicated indexes to verify the error message"); + + try { + objectStore.createIndex(indexName, keyPath); + + ok(false, "ConstraintError should be thrown if index already exists"); + } catch (e) { + ok(true, "ConstraintError should be thrown if index already exists"); + is( + e.message, + `IDBObjectStore.createIndex: Index named '${indexName}' already exists at index '0'`, + "Threw with correct error message" + ); + } + + await expectingSuccess(request); + db.close(); +} diff --git a/dom/indexedDB/test/unit/test_count.js b/dom/indexedDB/test/unit/test_count.js new file mode 100644 index 0000000000..c2a756c52f --- /dev/null +++ b/dom/indexedDB/test/unit/test_count.js @@ -0,0 +1,507 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..a925bd79d6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_index.js @@ -0,0 +1,127 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..5af2873ae6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_index_with_integer_keys.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..df85de3187 --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_locale_aware_index.js @@ -0,0 +1,153 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..fcdfb8642e --- /dev/null +++ b/dom/indexedDB/test/unit/test_create_objectStore.js @@ -0,0 +1,137 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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" + ); + is(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(); +} 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 0000000000..09099387f3 --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursor_cycle.js @@ -0,0 +1,48 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 0000000000..6af7df5c9b --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursor_mutation.js @@ -0,0 +1,125 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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. +} 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 0000000000..338ea7f3a5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursor_update_updates_indexes.js @@ -0,0 +1,118 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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"); + 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; + + 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"); + is( + 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"); + is( + obj.data, + event.target.result.data, + "Unique index was properly updated." + ); + + // Wait for success + yield undefined; + + db.close(); + } + + finishTest(); +} diff --git a/dom/indexedDB/test/unit/test_cursors.js b/dom/indexedDB/test/unit/test_cursors.js new file mode 100644 index 0000000000..15e3d94355 --- /dev/null +++ b/dom/indexedDB/test/unit/test_cursors.js @@ -0,0 +1,357 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + function checkCursor(cursor, expectedKey) { + is(cursor.key, expectedKey, "Correct key"); + is(cursor.primaryKey, expectedKey, "Correct primary key"); + is(cursor.value, "foo", "Correct value"); + } + + 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) { + checkCursor(cursor, sortedKeys[keyIndex]); + + 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"); + } + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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) { + checkCursor(cursor, sortedKeys[keyIndex]); + + cursor.continue(); + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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) { + checkCursor(cursor, sortedKeys[keyIndex]); + + if (keyIndex) { + cursor.continue(); + } else { + cursor.continue("b"); + } + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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) { + checkCursor(cursor, sortedKeys[keyIndex]); + + if (keyIndex) { + cursor.continue(); + } else { + cursor.continue(10); + } + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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) { + checkCursor(cursor, sortedKeys[keyIndex]); + + if (keyIndex) { + cursor.continue(); + } else { + cursor.continue("c"); + } + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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; + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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; + + request = objectStore.openCursor(null, "next"); + request.onerror = errorHandler; + storedCursor = null; + request.onsuccess = function (event) { + let cursor = event.target.result; + if (cursor) { + storedCursor = cursor; + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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 delete 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; + + checkCursor(cursor, sortedKeys[keyIndex]); + + cursor.continue(); + + checkCursor(cursor, sortedKeys[keyIndex]); + + 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(); +} 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 0000000000..7d4225fb33 --- /dev/null +++ b/dom/indexedDB/test/unit/test_database_close_without_onclose.js @@ -0,0 +1,51 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/* exported testGenerator */ +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 0000000000..0162ffc3ab --- /dev/null +++ b/dom/indexedDB/test/unit/test_database_onclose.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() { + 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.next(true); + }; + addRequest.onsuccess = function () { + testGenerator.next(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.next(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.next(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.next(false); + } + }; + + clearAllDatabases(() => { + info("clearAllDatabases is done."); + continueToNextStep(); + }); + + let 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(); +} diff --git a/dom/indexedDB/test/unit/test_deleteDatabase.js b/dom/indexedDB/test/unit/test_deleteDatabase.js new file mode 100644 index 0000000000..51e33ad974 --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase.js @@ -0,0 +1,108 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..3b26f2a4ef --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase_interactions.js @@ -0,0 +1,65 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..1b70fb113f --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase_onblocked.js @@ -0,0 +1,82 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..5e3321188d --- /dev/null +++ b/dom/indexedDB/test/unit/test_deleteDatabase_onblocked_duringVersionChange.js @@ -0,0 +1,83 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..f51d75edbb --- /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/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..5d4299a387 --- /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/ + */ + +/* exported testGenerator */ +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_getAll.js b/dom/indexedDB/test/unit/test_getAll.js new file mode 100644 index 0000000000..6ada30d845 --- /dev/null +++ b/dom/indexedDB/test/unit/test_getAll.js @@ -0,0 +1,198 @@ +/** + * 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(); +} 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 0000000000..8f3489773f --- /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/ + */ + +/* exported testGenerator */ +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 0000000000..2687675689 --- /dev/null +++ b/dom/indexedDB/test/unit/test_globalObjects_other.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() { + function getSpec(filename) { + let file = do_get_file(filename); + let uri = Services.io.newFileURI(file); + return uri.spec; + } + + // Test for IDBKeyRange and indexedDB availability in JS modules. + const { GlobalObjectsModule } = ChromeUtils.importESModule( + "resource://test/GlobalObjectsModule.sys.mjs" + ); + let test = new GlobalObjectsModule(); + 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 0000000000..98bb7b600f --- /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/ + */ + +/* exported testGenerator */ +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 0000000000..8cbc3fbef8 --- /dev/null +++ b/dom/indexedDB/test/unit/test_global_data.js @@ -0,0 +1,60 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_idbSubdirUpgrade.js b/dom/indexedDB/test/unit/test_idbSubdirUpgrade.js new file mode 100644 index 0000000000..5a6cf7686e --- /dev/null +++ b/dom/indexedDB/test/unit/test_idbSubdirUpgrade.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 }, + ]; + + for (let i = 1; i <= 2; i++) { + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("idbSubdirUpgrade" + i + "_profile"); + + for (let params of openParams) { + let request = indexedDB.openForPrincipal( + getPrincipal(params.url), + params.dbName, + params.dbVersion + ); + 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 = indexedDB.openForPrincipal( + getPrincipal(params.url), + params.dbName, + params.dbVersion + ); + 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 0000000000..b73dfb9848 --- /dev/null +++ b/dom/indexedDB/test/unit/test_idle_maintenance.js @@ -0,0 +1,178 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + let uri = Services.io.newURI("https://www.example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + info("Setting permissions"); + + Services.perms.addFromPrincipal( + principal, + "indexedDB", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + // The idle-daily notification is disabled in xpchsell tests, so we don't + // need to do anything special to disable it for this test. + + info("Activating real idle service"); + + do_get_idle(); + + info("Creating databases"); + + // Keep at least one database open. + let req = indexedDB.open("foo-a", 1); + req.onerror = errorHandler; + req.onsuccess = grabEventAndContinueHandler; + let event = yield undefined; + + // 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; + + 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; + + Services.qms.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 = Services.qms.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; + + Services.qms.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 0000000000..1e13f4d824 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_empty_keyPath.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 : "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.next(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"); + let 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(); +} 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 0000000000..95e4e70c95 --- /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 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.next(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(); +} 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 0000000000..9459cd4de6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_getAllObjects.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() { + 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 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.next(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(); +} 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 0000000000..850e1c6007 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_object_cursors.js @@ -0,0 +1,158 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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]; + 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(); +} 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 0000000000..da43feb3b9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_index_update_delete.js @@ -0,0 +1,172 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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, + }); + + for (let i = 0; i < 10; i++) { + objectStore.add({ id: i, index: i }); + } + + for (let unique of [false, true]) { + objectStore.createIndex(unique, "index", { 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. +} diff --git a/dom/indexedDB/test/unit/test_indexes.js b/dom/indexedDB/test/unit/test_indexes.js new file mode 100644 index 0000000000..7adb57f77a --- /dev/null +++ b/dom/indexedDB/test/unit/test_indexes.js @@ -0,0 +1,1649 @@ +/** + * 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.next(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, "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(); +} 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 0000000000..c695490f74 --- /dev/null +++ b/dom/indexedDB/test/unit/test_indexes_bad_values.js @@ -0,0 +1,140 @@ +/** + * 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.next(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(); +} 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 0000000000..953920dfe0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_indexes_funny_things.js @@ -0,0 +1,188 @@ +/** + * 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(); + return; + } + + 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.next(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, "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(); +} 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 0000000000..891a7b1446 --- /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/ + */ + +/* exported testGenerator, disableWorkerTest */ +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(); +} 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 0000000000..79d74ab2cc --- /dev/null +++ b/dom/indexedDB/test/unit/test_invalid_version.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_invalidate.js b/dom/indexedDB/test/unit/test_invalidate.js new file mode 100644 index 0000000000..5058756ff5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_invalidate.js @@ -0,0 +1,85 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 0000000000..1e5b8f22c1 --- /dev/null +++ b/dom/indexedDB/test/unit/test_key_requirements.js @@ -0,0 +1,266 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); + }); + + 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(); +} diff --git a/dom/indexedDB/test/unit/test_keys.js b/dom/indexedDB/test/unit/test_keys.js new file mode 100644 index 0000000000..e3f8d6abba --- /dev/null +++ b/dom/indexedDB/test/unit/test_keys.js @@ -0,0 +1,350 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +// helper function that ensures that ArrayBuffer instances are meaningfully +// displayed (not just as 'object ArrayBuffer') +// TODO better move to helpers.js? +function showKey(key) { + if (key instanceof Array) { + return key.map(x => showKey(x)).toString(); + } + if (key instanceof ArrayBuffer) { + return "ArrayBuffer([" + new Uint8Array(key).toString() + "])"; + } + return key.toString(); +} + +function* testSteps() { + const dbname = this.window ? window.location.pathname : "Splendid Test"; + + let openRequest = indexedDB.open(dbname, 1); + openRequest.onerror = errorHandler; + openRequest.onupgradeneeded = grabEventAndContinueHandler; + openRequest.onsuccess = unexpectedSuccessHandler; + let event = yield undefined; + let db = event.target.result; + + // Create test stores + let store = db.createObjectStore("store"); + let enc = new TextEncoder(); + + // Test simple inserts + // Note: the keys must be in order + 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", + // Note: enc.encode returns an Uint8Array, which is a valid key, but when + // converting it back and forth, the result will be a plain ArrayBuffer, + // which is expected in comparisons below + // TODO is it ok that the information that the original key was an + // Uint8Array is lost? + new ArrayBuffer(0), + Uint8Array.from([0]).buffer, + Uint8Array.from([0, 0]).buffer, + Uint8Array.from([0, 1]).buffer, + Uint8Array.from([0, 1, 0]).buffer, + enc.encode("abc").buffer, + enc.encode("abcd").buffer, + enc.encode("xyz").buffer, + Uint8Array.from([0x80]).buffer, + [], + [-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", [[[]]]], + // see comment on scalar ArrayBuffers above + [new ArrayBuffer(0)], + [new ArrayBuffer(0), "abc"], + [new ArrayBuffer(0), new ArrayBuffer(0)], + [new ArrayBuffer(0), enc.encode("abc").buffer], + [enc.encode("abc").buffer], + [enc.encode("abc").buffer, new ArrayBuffer(0)], + [enc.encode("abc").buffer, enc.encode("xyz").buffer], + [enc.encode("xyz").buffer], + [[]], + [[], "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; index = " + + i + + ", input = " + + showKey(keyI) + + ", returned = " + + showKey(e.target.result) + ); + ok( + compareKeys(e.target.result, keyI), + "Returned key should actually be equal; index = " + + i + + ", input = " + + showKey(keyI) + + ", returned = " + + showKey(e.target.result) + ); + }; + + // 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; index = " + + i + + ", input = " + + showKey(keys[i]) + + ", readBack = " + + showKey(cursor.key) + ); + ok( + compareKeys(cursor.key, keys[i]), + "Read back key should actually be equal; index = " + + i + + ", input = " + + showKey(keys[i]) + + ", readBack = " + + showKey(cursor.key) + ); + is(cursor.value, i, "Stored with right value"); + + cursor.continue(); + } + event = yield undefined; + is(event.target.result, null, "no more results expected"); + + // Note that nan is defined below as '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, [{}]]", + // ATTENTION, the following key allocates 2GB of memory and might cause + // subtle failures in some environments, see bug 1796753. We might + // want to have some common way between IndexeDB mochitests and + // xpcshell tests how to access AppConstants in order to dynamically + // exclude this key from some environments, rather than disabling the + // entire xpcshell variant of this test for ASAN/TSAN. + "new Uint8Array(2147483647)", + ]; + + function checkInvalidKeyException(ex, i, callText) { + let suffix = ` during ${callText} with invalid key ${i}: ${invalidKeys[i]}`; + // isInstance() is not available in mochitest, and we use this JS also as mochitest. + // eslint-disable-next-line mozilla/use-isInstance + ok(ex instanceof DOMException, "Threw DOMException" + suffix); + is(ex.name, "DataError", "Threw right DOMException" + suffix); + is(ex.code, 0, "Threw with right code" + suffix); + } + + for (i = 0; i < invalidKeys.length; ++i) { + let key_fn = Function( + `"use strict"; var nan = 0 / 0; let k = (${invalidKeys[i]}); return k;` + ); + let key; + try { + key = key_fn(); + } catch (e) { + // If we cannot instantiate the key, we are most likely on a 32 Bit + // platform with insufficient memory. Just skip it. + info("Key instantiation failed, skipping"); + continue; + } + try { + indexedDB.cmp(key, 1); + ok(false, "didn't throw"); + } catch (ex) { + checkInvalidKeyException(ex, i, "cmp(key, 1)"); + } + try { + indexedDB.cmp(1, key); + ok(false, "didn't throw2"); + } catch (ex) { + checkInvalidKeyException(ex, i, "cmp(1, key)"); + } + try { + store.put(1, key); + ok(false, "didn't throw3"); + } catch (ex) { + checkInvalidKeyException(ex, i, "store.put(1, key)"); + } + } + + openRequest.onsuccess = grabEventAndContinueHandler; + yield undefined; + + finishTest(); +} 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 0000000000..c73e9d77e6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_locale_aware_index_getAll.js @@ -0,0 +1,212 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 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.next(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(); +} 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 0000000000..be51d3bf2e --- /dev/null +++ b/dom/indexedDB/test/unit/test_locale_aware_index_getAllObjects.js @@ -0,0 +1,239 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 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.next(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(); +} 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 0000000000..69b4c8935f --- /dev/null +++ b/dom/indexedDB/test/unit/test_locale_aware_indexes.js @@ -0,0 +1,1688 @@ +/** + * 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.next(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, "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(); +} diff --git a/dom/indexedDB/test/unit/test_marker_file.js b/dom/indexedDB/test/unit/test_marker_file.js new file mode 100644 index 0000000000..342f78f34d --- /dev/null +++ b/dom/indexedDB/test/unit/test_marker_file.js @@ -0,0 +1,91 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function getTestingFiles() { + const filenameBase = "3128029391StpsleeTn+ddi"; + let baseDir = getRelativeFile("storage/permanent/chrome/idb"); + + let dbFile = baseDir.clone(); + dbFile.append(filenameBase + ".sqlite"); + + let dir = baseDir.clone(); + dir.append(filenameBase + ".files"); + + let markerFile = baseDir.clone(); + markerFile.append("idb-deleting-" + filenameBase); + + return { dbFile, dir, markerFile }; +} + +function createTestingEnvironment(markerFileOnly = false) { + let testingFiles = getTestingFiles(); + + if (!markerFileOnly) { + testingFiles.dbFile.create( + Ci.nsIFile.NORMAL_FILE_TYPE, + parseInt("0644", 8) + ); + + testingFiles.dir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8)); + } + + testingFiles.markerFile.create( + Ci.nsIFile.NORMAL_FILE_TYPE, + parseInt("0644", 8) + ); +} + +/** + * This test verifies the initialization, indexedDB.open(), and + * indexedDB.deleteDatabase() aren't blocked when there are unexpected files + * which haven't been deleted due to some reasons. Normally, we expect every + * delete operation works fine. Hoever, it's reported that there is only a + * directory without a corresponding database. It's probably because the delete + * operation fails for some reasons. P1 introduces the mark-file to let the + * future operation understand whether there might be unexpected files in idb + * directory. And, this test verifies these three things work fine if a + * marker-file, a databse file, and a directory exist in current idb directory. + */ + +/* exported testSteps */ +async function testSteps() { + SpecialPowers.setBoolPref("dom.quotaManager.testing", true); + + const name = this.window ? window.location.pathname : "Splendid Test"; + + info("Verifying initialization"); + + let request = initStorage(); + await requestFinished(request); + + createTestingEnvironment(); + + request = initPersistentOrigin(getSystemPrincipal()); + await requestFinished(request); + + let testingFiles = getTestingFiles(); + ok(!testingFiles.dbFile.exists(), "The obsolete database file doesn't exist"); + ok(!testingFiles.dir.exists(), "The obsolete directory doesn't exist"); + ok(!testingFiles.markerFile.exists(), "The marker file doesn't exist"); + + info("Verifying open shouldn't be blocked by unexpected files"); + + createTestingEnvironment(); + + request = indexedDB.open(name); + await expectingUpgrade(request); + let event = await expectingSuccess(request); + ok(true, "The database was opened successfully"); + let db = event.target.result; + db.close(); + + info("Verifying deleteDatabase isn't blocked by unexpected files"); + + createTestingEnvironment(true); + + request = indexedDB.deleteDatabase(name); + await expectingSuccess(request); + ok(true, "The database was deleted successfully"); +} 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 0000000000..64ee2a0b9a --- /dev/null +++ b/dom/indexedDB/test/unit/test_maximal_serialized_object_size.js @@ -0,0 +1,109 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator, disableWorkerTest */ +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; + + 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( + `IDBObjectStore.${aOperation}: 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(); +} diff --git a/dom/indexedDB/test/unit/test_metadata2Restore.js b/dom/indexedDB/test/unit/test_metadata2Restore.js new file mode 100644 index 0000000000..da31eecc86 --- /dev/null +++ b/dom/indexedDB/test/unit/test_metadata2Restore.js @@ -0,0 +1,328 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + const openParams = [ + // 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" }, + }, + ]; + + function openDatabase(params) { + let request; + if ("url" in params) { + let uri = Services.io.newURI(params.url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + 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 0000000000..001d4da65b --- /dev/null +++ b/dom/indexedDB/test/unit/test_metadataRestore.js @@ -0,0 +1,131 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + const openParams = [ + // 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" }, + }, + ]; + + function openDatabase(params) { + let request; + if ("url" in params) { + let uri = Services.io.newURI(params.url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + 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 0000000000..7641da7712 --- /dev/null +++ b/dom/indexedDB/test/unit/test_multientry.js @@ -0,0 +1,271 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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; + yield undefined; + + req = index.openKeyCursor(); + req.onsuccess = grabEventAndContinueHandler; + for (let j = 0; j < test.indexes.length; ++j) { + 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(); + } + 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) { + 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(); + } + 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; + 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(); + } + + req = index.openKeyCursor(); + req.onsuccess = grabEventAndContinueHandler; + for (let j = 0; j < indexes.length; ++j) { + 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(); + } + 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) { + 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(); + } + 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(); +} diff --git a/dom/indexedDB/test/unit/test_mutableFileUpgrade.js b/dom/indexedDB/test/unit/test_mutableFileUpgrade.js new file mode 100644 index 0000000000..1875b29924 --- /dev/null +++ b/dom/indexedDB/test/unit/test_mutableFileUpgrade.js @@ -0,0 +1,128 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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; + + try { + event.target.result; + ok(false, "IDBMutableFile must not be read"); + } catch (err) { + is(err.name, "InvalidStateError", "Wrong error type"); + } + + request = db + .transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(5); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + try { + event.target.result; + ok(false, "IDBMutableFile must not be read"); + } catch (err) { + is(err.name, "InvalidStateError", "Wrong error type"); + } + + request = db + .transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(6); + request.onsuccess = grabEventAndContinueHandler; + event = yield undefined; + + try { + event.target.result; + ok(false, "IDBMutableFile must not be read"); + } catch (err) { + is(err.name, "InvalidStateError", "Wrong error type"); + } + + 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 0000000000..d89fcd7f3a --- /dev/null +++ b/dom/indexedDB/test/unit/test_names_sorted.js @@ -0,0 +1,128 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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. + for (let j = 0; j < indexInfo.length; j++) { + let info = indexInfo[j]; + 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(); +} diff --git a/dom/indexedDB/test/unit/test_objectCursors.js b/dom/indexedDB/test/unit/test_objectCursors.js new file mode 100644 index 0000000000..10d8c83c6e --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectCursors.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"; + + 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(); +} 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 0000000000..15d69e7b93 --- /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/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..8446fcd5d0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_inline_autoincrement_key_added_on_put.js @@ -0,0 +1,57 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..3794bb7cca --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_openKeyCursor.js @@ -0,0 +1,401 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..7bf5c21b33 --- /dev/null +++ b/dom/indexedDB/test/unit/test_objectStore_remove_values.js @@ -0,0 +1,98 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..6b23fa5243 --- /dev/null +++ b/dom/indexedDB/test/unit/test_object_identity.js @@ -0,0 +1,49 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_obsoleteOriginAttributesUpgrade.js b/dom/indexedDB/test/unit/test_obsoleteOriginAttributesUpgrade.js new file mode 100644 index 0000000000..893dfb08a5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_obsoleteOriginAttributesUpgrade.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + const url = "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f"; + const dbName = "Hello."; + const dbVersion = 1; + + clearAllDatabases(continueToNextStepSync); + yield; + + // The origin directory contained in the package is: + // "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com" + installPackagedProfile("obsoleteOriginAttributes_profile"); + + let request = indexedDB.openForPrincipal( + getPrincipal(url), + dbName, + dbVersion + ); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + let event = yield; + + is(event.type, "success", "Correct event type"); + + resetAllDatabases(continueToNextStepSync); + yield; + + request = indexedDB.openForPrincipal(getPrincipal(url), dbName, dbVersion); + request.onerror = errorHandler; + request.onupgradeneeded = unexpectedSuccessHandler; + request.onsuccess = grabEventAndContinueHandler; + event = yield; + + is(event.type, "success", "Correct event type"); + + finishTest(); +} 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 0000000000..e7462a87fd --- /dev/null +++ b/dom/indexedDB/test/unit/test_odd_result_order.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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, + }); + 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(); +} diff --git a/dom/indexedDB/test/unit/test_oldDirectories.js b/dom/indexedDB/test/unit/test_oldDirectories.js new file mode 100644 index 0000000000..1649bd63a5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_oldDirectories.js @@ -0,0 +1,66 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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; + + function openDatabase() { + let uri = Services.io.newURI(url); + let principal = Services.scriptSecurityManager.createContentPrincipal( + 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 profileDir = Services.dirsvc.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 0000000000..e7bc09a991 --- /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/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..657045364e --- /dev/null +++ b/dom/indexedDB/test/unit/test_open_for_principal.js @@ -0,0 +1,89 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 = Services.io.newURI("http://appdata.example.com"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + 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 0000000000..1908edc469 --- /dev/null +++ b/dom/indexedDB/test/unit/test_open_objectStore.js @@ -0,0 +1,36 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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"); + is(objectStore.indexNames.length, 0, "Bad indexNames"); + + finishTest(); +} diff --git a/dom/indexedDB/test/unit/test_optionalArguments.js b/dom/indexedDB/test/unit/test_optionalArguments.js new file mode 100644 index 0000000000..1f15335b18 --- /dev/null +++ b/dom/indexedDB/test/unit/test_optionalArguments.js @@ -0,0 +1,1796 @@ +/** + * 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(); +} diff --git a/dom/indexedDB/test/unit/test_orphaned_files.js b/dom/indexedDB/test/unit/test_orphaned_files.js new file mode 100644 index 0000000000..fc4592c356 --- /dev/null +++ b/dom/indexedDB/test/unit/test_orphaned_files.js @@ -0,0 +1,59 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * The goal of this test is to prove that orphaned files are cleaned up during + * origin initialization. A file is orphaned when there's a file with zero size + * in the $dbName.files/journals directory and the file table in the database + * contains no records for given id. A file can become orphaned when we didn't + * have a chance to remove the file from disk during shutdown or the app just + * crashed. + */ + +/* exported testSteps */ +async function testSteps() { + const name = "test_orphaned_files.js"; + + const objectStoreName = "Blobs"; + + const blobData = { key: 1 }; + + info("Installing profile"); + + let request = clearAllDatabases(); + await requestFinished(request); + + // The profile contains one initialized origin directory (with an IndexedDB + // database and an orphaned file), a script for origin initialization and the + // storage database: + // - storage/permanent/chrome + // - create_db.js + // - storage.sqlite + // 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 + // Note: to make it become the profile in the test, additional manual steps + // are needed. + // 1. Remove the file "storage/ls-archive.sqlite". + + installPackagedProfile("orphaned_files_profile"); + + info("Opening database"); + + request = indexedDB.open(name); + await expectingSuccess(request); + + info("Getting data"); + + request = request.result + .transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(blobData.key); + await requestSucceeded(request); + + info("Verifying data"); + + ok(request.result === undefined, "Correct result"); +} 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 0000000000..0fe6b734c1 --- /dev/null +++ b/dom/indexedDB/test/unit/test_overlapping_transactions.js @@ -0,0 +1,93 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..364a3c12e5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_put_get_values.js @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..fe91bf5582 --- /dev/null +++ b/dom/indexedDB/test/unit/test_put_get_values_autoIncrement.js @@ -0,0 +1,52 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..20366be417 --- /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/ + */ + +/* exported testGenerator, disableWorkerTest */ +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.next(true); + }; + trans.onabort = function (event) { + is(trans.error.name, "QuotaExceededError", "Reached quota limit"); + testGenerator.next(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 0000000000..8252ae028f --- /dev/null +++ b/dom/indexedDB/test/unit/test_readonly_transactions.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() { + 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(); +} 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 0000000000..6271959346 --- /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/ + */ + +/* exported testGenerator, disableWorkerTest */ +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 { + 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, + "IDBDatabase.transaction: 'readwriteflush' (value of argument 2) is not " + + "a valid value for enumeration IDBTransactionMode.", + "exception has the correct message" + ); + + finishTest(); +} 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 0000000000..5cdd096bab --- /dev/null +++ b/dom/indexedDB/test/unit/test_remove_index.js @@ -0,0 +1,56 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..bdc16d9a6a --- /dev/null +++ b/dom/indexedDB/test/unit/test_remove_objectStore.js @@ -0,0 +1,131 @@ +/** + * 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.next(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(); +} 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 0000000000..fb4af954b3 --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_index.js @@ -0,0 +1,196 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..34a83fe22d --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_index_errors.js @@ -0,0 +1,137 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..6d3524b8ff --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_objectStore.js @@ -0,0 +1,173 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..d7b56053e7 --- /dev/null +++ b/dom/indexedDB/test/unit/test_rename_objectStore_errors.js @@ -0,0 +1,135 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..db42c4e349 --- /dev/null +++ b/dom/indexedDB/test/unit/test_request_readyState.js @@ -0,0 +1,50 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_sandbox.js b/dom/indexedDB/test/unit/test_sandbox.js new file mode 100644 index 0000000000..20b34cebb5 --- /dev/null +++ b/dom/indexedDB/test/unit/test_sandbox.js @@ -0,0 +1,83 @@ +/* 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() { + 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(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(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(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 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 0000000000..af44a42b81 --- /dev/null +++ b/dom/indexedDB/test/unit/test_schema18upgrade.js @@ -0,0 +1,370 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 0000000000..c38e2fb08f --- /dev/null +++ b/dom/indexedDB/test/unit/test_schema21upgrade.js @@ -0,0 +1,370 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 0000000000..fdd1c4c51b --- /dev/null +++ b/dom/indexedDB/test/unit/test_schema23upgrade.js @@ -0,0 +1,53 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 }, + ]; + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("schema23upgrade_profile"); + + for (let params of openParams) { + let request = indexedDB.openForPrincipal( + getPrincipal(params.url), + params.dbName, + params.dbVersion + ); + 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 = indexedDB.openForPrincipal( + getPrincipal(params.url), + params.dbName, + params.dbVersion + ); + 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 0000000000..f1b50e7bb1 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..fe5d63fea2 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_abort.js @@ -0,0 +1,96 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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"); + 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"); + 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(); +} 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 0000000000..d80edcca31 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_events.js @@ -0,0 +1,171 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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"); + } else { + throw e; + } + } + + 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(); + }); + + // 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"); + }); + + // 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(); +} 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 0000000000..a084375fa0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_setVersion_exclusion.js @@ -0,0 +1,97 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..2706c2c2a4 --- /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/ + */ + +/* exported testGenerator */ +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); + // eslint-disable-next-line no-undef + 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 DOMException, "Request has a DOMException"); + is(event.target.error.name, "AbortError", "Request has AbortError"); + + finishTest(); +} diff --git a/dom/indexedDB/test/unit/test_snappyUpgrade.js b/dom/indexedDB/test/unit/test_snappyUpgrade.js new file mode 100644 index 0000000000..5d73b3c3c0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_snappyUpgrade.js @@ -0,0 +1,47 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 0000000000..ebde969344 --- /dev/null +++ b/dom/indexedDB/test/unit/test_storagePersistentUpgrade.js @@ -0,0 +1,53 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 }, + ]; + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + installPackagedProfile("storagePersistentUpgrade_profile"); + + for (let params of openParams) { + let request = indexedDB.openForPrincipal( + getPrincipal(params.url), + params.dbName, + params.dbVersion + ); + 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 = indexedDB.openForPrincipal( + getPrincipal(params.url), + params.dbName, + params.dbVersion + ); + 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 0000000000..14bd4080f0 --- /dev/null +++ b/dom/indexedDB/test/unit/test_storage_manager_estimate.js @@ -0,0 +1,94 @@ +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.next(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.next(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.next(estimation.usage); + }); + let usageAfterPut = yield undefined; + ok( + usageAfterPut > usageAfterCreate, + "estimated usage must increase after putting large object" + ); + db.close(); + + finishTest(); +} + +/* exported setup */ +async function setup(isXOrigin) { + // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5) + // Acquire storage access permission here so that the iframe has + // first-party access to the storage estimate. Without this, it is + // isolated and this test will always fail + if (isXOrigin) { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + }); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await SpecialPowers.addPermission( + "storageAccessAPI", + true, + window.location.href + ); + await SpecialPowers.wrap(document).requestStorageAccess(); + } + 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 0000000000..034e8ff658 --- /dev/null +++ b/dom/indexedDB/test/unit/test_success_events_after_abort.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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. + SpecialPowers.Services.tm.spinEventLoopUntilEmpty(); + } + + finishTest(); +} 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 0000000000..e1f3b5e7d6 --- /dev/null +++ b/dom/indexedDB/test/unit/test_table_locks.js @@ -0,0 +1,123 @@ +/** + * 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; + +/* exported testGenerator */ +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(); +} + +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 0000000000..07262db16c --- /dev/null +++ b/dom/indexedDB/test/unit/test_table_rollback.js @@ -0,0 +1,118 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..4853ebbd14 --- /dev/null +++ b/dom/indexedDB/test/unit/test_temporary_storage.js @@ -0,0 +1,279 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +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.next(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.next(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.next(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.next(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 0000000000..6505b66964 --- /dev/null +++ b/dom/indexedDB/test/unit/test_traffic_jam.js @@ -0,0 +1,99 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..6829392842 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_abort.js @@ -0,0 +1,377 @@ +/** + * 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(); +} 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 0000000000..6a2c61128b --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_abort_hang.js @@ -0,0 +1,108 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var self = this; + +/* exported testGenerator */ +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(); +} 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 0000000000..6b084fa143 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_duplicate_store_names.js @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} 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 0000000000..6b134a97ae --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_error.js @@ -0,0 +1,131 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 TypeError, "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()"); + + 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(); +} 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 0000000000..12150397b9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_lifetimes.js @@ -0,0 +1,97 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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 }); + 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(); +} 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 0000000000..88ac61b58c --- /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/ + */ + +/* exported testGenerator, disableWorkerTest */ +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; + + db.transaction("foo"); + + let transaction2; + + let eventHasRun; + + let tm = SpecialPowers.Services ? SpecialPowers.Services.tm : Services.tm; + + tm.dispatchToMainThread(function () { + eventHasRun = true; + + transaction2 = db.transaction("foo"); + }); + + tm.spinEventLoopUntil( + "Test(test_transaction_lifetimes_nested.js:testSteps)", + () => eventHasRun + ); + + ok(transaction2, "Non-null transaction2"); + + continueToNextStep(); + yield undefined; + + finishTest(); +} 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 0000000000..beb9dcfdc3 --- /dev/null +++ b/dom/indexedDB/test/unit/test_transaction_ordering.js @@ -0,0 +1,50 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); +} diff --git a/dom/indexedDB/test/unit/test_unexpectedDirectory.js b/dom/indexedDB/test/unit/test_unexpectedDirectory.js new file mode 100644 index 0000000000..5af56da1c8 --- /dev/null +++ b/dom/indexedDB/test/unit/test_unexpectedDirectory.js @@ -0,0 +1,63 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function getTestingFiles() { + const filenameBase = "unexpectedDirectory"; + let baseDir = getRelativeFile("storage/permanent/chrome/idb"); + + let unexpectedDirWithoutSuffix = baseDir.clone(); + unexpectedDirWithoutSuffix.append(filenameBase); + + let unexpectedDir = baseDir.clone(); + unexpectedDir.append(filenameBase + ".files"); + + return { unexpectedDirWithoutSuffix, unexpectedDir }; +} + +function createTestingEnvironment() { + let testingFiles = getTestingFiles(); + testingFiles.unexpectedDir.create( + Ci.nsIFile.DIRECTORY_TYPE, + parseInt("0755", 8) + ); + + testingFiles.unexpectedDirWithoutSuffix.create( + Ci.nsIFile.DIRECTORY_TYPE, + parseInt("0755", 8) + ); +} + +/** + * This test verifies unexpected directories won't block idb's initialization. + */ + +/* exported testSteps */ +async function testSteps() { + info("Verifying open shouldn't be blocked by unexpected files"); + + createTestingEnvironment(); + + let request = indexedDB.open( + this.window ? window.location.pathname : "Splendid Test", + 1 + ); + await expectingUpgrade(request); + + // Waiting for a success event for indexedDB.open() + let event = await expectingSuccess(request); + + let testingFiles = getTestingFiles(); + ok( + !testingFiles.unexpectedDir.exists(), + "The unexpected directory doesn't exist" + ); + ok( + !testingFiles.unexpectedDirWithoutSuffix.exists(), + "The unexpected directory without the suffix doesn't exist" + ); + + let db = event.target.result; + db.close(); +} 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 0000000000..736344ae5e --- /dev/null +++ b/dom/indexedDB/test/unit/test_unique_index_update.js @@ -0,0 +1,71 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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, + }); + 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]) { + let 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(); +} diff --git a/dom/indexedDB/test/unit/test_upgrade_add_index.js b/dom/indexedDB/test/unit/test_upgrade_add_index.js new file mode 100644 index 0000000000..06b9ae7bff --- /dev/null +++ b/dom/indexedDB/test/unit/test_upgrade_add_index.js @@ -0,0 +1,107 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function generateKey() { + let algorithm = { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + modulusLength: 1024, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + }; + + return crypto.subtle.generateKey(algorithm, true, ["sign", "verify"]); +} + +const hasCrypto = "crypto" in this; + +/** + * Test addition of a new index when the existing values in the referenced + * object store have potentially unusual structured clone participants. + * + * When a new index is created, the existing object store's contents need to be + * processed to derive the index values. This is a special event because + * normally index values are extracted at add()/put() time in the content + * process using the page/worker's JS context (modulo some spec stuff). But + * the index creation operation is actually running in the parent process on the + * I/O thread for the given database. So the JS context scenario is suddenly + * a lot more complicated, and we need extra test coverage, in particular for + * unusual structured clone payloads. + * + * Relationship to other test: + * - test_create_index_with_integer_keys.js: This test is derived from that one. + */ +function* testSteps() { + // -- Create our fancy data that has interesting structured clone issues. + const allData = []; + + // the xpcshell tests normalize self into existence. + if (hasCrypto) { + info("creating crypto key"); + // (all IDB tests badly need a test driver update...) + generateKey().then(grabEventAndContinueHandler); + let key = yield undefined; + allData.push({ + id: 1, + what: "crypto", + data: key, + }); + } else { + info("not storing crypto key"); + } + + // -- Create the IDB and populate it with the base data. + info("opening initial database"); + let request = indexedDB.open( + this.window ? window.location.pathname : "Splendid Test", + 1 + ); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; // advance onupgradeneeded + let event = yield undefined; // wait for onupgradeneeded. + + let db = event.target.result; + db.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; // advance when the open completes + + // Make object store, add data. + let objectStore = db.createObjectStore("foo", { keyPath: "id" }); + for (let datum of allData) { + info(`add()ing ${datum.what}`); + objectStore.add(datum); + } + // wait for the open to complete following our upgrade transaction self-closing + yield undefined; + // explicitly close the database so we can open it again. We don't wait for + // this, but the upgrade open will block until the close actually happens. + info("closing initial database"); + db.close(); + + // -- Trigger an upgrade, adding a new index. + info("opening database for upgrade to v2"); + request = indexedDB.open( + this.window ? window.location.pathname : "Splendid Test", + 2 + ); + request.onerror = errorHandler; + request.onupgradeneeded = grabEventAndContinueHandler; // advance onupgradeneeded + event = yield undefined; // wait for onupgradeneeded + + let db2 = event.target.result; + db2.onerror = errorHandler; + + event.target.onsuccess = continueToNextStep; // advance when the open completes + + // Create index. + info("in upgrade, creating index"); + event.target.transaction.objectStore("foo").createIndex("foo", "what"); + yield undefined; // wait for the open to complete + info("upgrade completed"); + + finishTest(); +} 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 0000000000..a270e65f2f --- /dev/null +++ b/dom/indexedDB/test/unit/test_view_put_get_values.js @@ -0,0 +1,151 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator, disableWorkerTest */ +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) }; + + const tests = [ + { + external: false, + preprocessing: false, + }, + { + external: true, + preprocessing: false, + }, + { + external: true, + preprocessing: true, + }, + ]; + + for (let test of tests) { + if (test.external) { + if (this.window) { + info("Setting data threshold pref"); + + SpecialPowers.pushPrefEnv( + { set: [["dom.indexedDB.dataThreshold", 0]] }, + continueToNextStep + ); + yield undefined; + } else { + setDataThreshold(0); + } + } + + if (test.preprocessing) { + if (this.window) { + info("Setting preprocessing pref"); + + SpecialPowers.pushPrefEnv( + { set: [["dom.indexedDB.preprocessing", true]] }, + continueToNextStep + ); + yield undefined; + } else { + enablePreprocessing(); + } + } + + 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 (test.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; + + if (this.window) { + info("Resetting prefs"); + + SpecialPowers.popPrefEnv(continueToNextStep); + yield undefined; + } else { + if (test.external) { + resetDataThreshold(); + } + + if (test.preprocessing) { + resetPreprocessing(); + } + } + } + + finishTest(); +} diff --git a/dom/indexedDB/test/unit/test_wasm_get_values.js b/dom/indexedDB/test/unit/test_wasm_get_values.js new file mode 100644 index 0000000000..676cb1662b --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_get_values.js @@ -0,0 +1,59 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +var testGenerator = testSteps(); + +function* testSteps() { + const name = "test_wasm_recompile.js"; + + const objectStoreName = "Wasm"; + + const wasmData = { key: 1 }; + + // The goal of this test is to prove that stored wasm is never deserialized. + + info("Installing profile"); + + clearAllDatabases(continueToNextStepSync); + yield undefined; + + // The profile was created with a mythical build (buildId: 20180309213541, + // 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_get_values_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 wasm"); + + request = db + .transaction([objectStoreName]) + .objectStore(objectStoreName) + .get(wasmData.key); + request.onsuccess = continueToNextStepSync; + yield undefined; + + info("Verifying wasm"); + + let isWasmModule = request.result instanceof WebAssembly.Module; + ok(!isWasmModule, "Object is not wasm module"); + + 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 0000000000..13d35c7d7a --- /dev/null +++ b/dom/indexedDB/test/unit/test_wasm_put_get_values.js @@ -0,0 +1,69 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testGenerator */ +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(); + return; + } + + // js -e 'print(wasmTextToBinary(`(module (func (export "run") (result i32) (i32.const 13)))`))' + // prettier-ignore + let binary = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,114,117,110,0,0,10,6,1,4,0,65,13,11]); + + 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("Testing failure to store wasm"); + + let objectStore = db + .transaction([objectStoreName], "readwrite") + .objectStore(objectStoreName); + + // storing a wasm module in IDB should now fail + let failed = false; + try { + objectStore.add(wasmData.value, wasmData.key); + } catch (err) { + failed = true; + ok(err instanceof DOMException, "caught right error type"); + is(err.name, "DataCloneError", "caught right error name"); + } + ok(failed, "error was thrown"); + + finishTest(); +} 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 0000000000..aa036671d9 --- /dev/null +++ b/dom/indexedDB/test/unit/test_writer_starvation.js @@ -0,0 +1,117 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +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_get_values_profile.zip b/dom/indexedDB/test/unit/wasm_get_values_profile.zip Binary files differnew file mode 100644 index 0000000000..916f7414d4 --- /dev/null +++ b/dom/indexedDB/test/unit/wasm_get_values_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 0000000000..d8490e29b5 --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-child-process.ini @@ -0,0 +1,17 @@ +# 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' +support-files = + GlobalObjectsChild.js + GlobalObjectsModule.sys.mjs + 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 0000000000..4b40bc5c73 --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-head-child-process.js @@ -0,0 +1,23 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function run_test() { + 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); + + // This is defined globally via xpcshell. + /* global _HEAD_FILES */ + _HEAD_FILES.push(do_get_file(INDEXEDDB_HEAD_FILE).path.replace(/\\/g, "/")); + + Services.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 0000000000..6b7683969d --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js @@ -0,0 +1,738 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests using testGenerator are expected to define it themselves. +// Testing functions are expected to call testSteps and its type should either +// be GeneratorFunction or AsyncFunction +/* global testGenerator, testSteps:false */ + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +if (!("self" in this)) { + this.self = this; +} + +var bufferCache = []; + +function is(a, b, msg) { + Assert.equal(a, b, msg); +} + +function ok(cond, msg) { + Assert.ok(!!cond, msg); +} + +function isnot(a, b, msg) { + Assert.notEqual(a, b, msg); +} + +function todo(condition, name, diag) { + todo_check_true(condition); +} + +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(); + } + + Cu.importGlobalProperties(["indexedDB"]); + + // In order to support converting tests to using async functions from using + // generator functions, we detect async functions by checking the name of + // function's constructor. + Assert.ok( + typeof testSteps === "function", + "There should be a testSteps function" + ); + if (testSteps.constructor.name === "AsyncFunction") { + // Do run our existing cleanup function that would normally be called by + // the generator's call to finishTest(). + registerCleanupFunction(resetTesting); + + add_task(testSteps); + + // Since we defined run_test, we must invoke run_next_test() to start the + // async test. + run_next_test(); + } else { + Assert.ok( + testSteps.constructor.name === "GeneratorFunction", + "Unsupported function type" + ); + + do_test_pending(); + testGenerator.next(); + } + }; +} + +function finishTest() { + if (SpecialPowers.isMainProcess()) { + resetExperimental(); + resetTesting(); + } + + SpecialPowers.removeFiles(); + + executeSoon(function () { + do_test_finished(); + }); +} + +function grabEventAndContinueHandler(event) { + testGenerator.next(event); +} + +function continueToNextStep() { + executeSoon(function () { + testGenerator.next(); + }); +} + +function errorHandler(event) { + try { + dump("indexedDB error: " + event.target.error.name); + } catch (e) { + dump("indexedDB error: " + e); + } + Assert.ok(false); + finishTest(); +} + +function unexpectedSuccessHandler() { + Assert.ok(false); + finishTest(); +} + +function expectedErrorHandler(name) { + return function (event) { + Assert.equal(event.type, "error"); + Assert.equal(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(event) { + Assert.equal(event.type, "error"); + Assert.equal(this._name, event.target.error.name); + if (this._preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } + grabEventAndContinueHandler(event); + }, +}; + +function continueToNextStepSync() { + testGenerator.next(); +} + +// TODO compareKeys is duplicated in ../helpers.js, can we import that here? +// the same applies to many other functions in this file +// this duplication should be avoided (bug 1565986) +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; + } + + if (k1 instanceof ArrayBuffer) { + if (!(k2 instanceof ArrayBuffer)) { + return false; + } + + function arrayBuffersAreEqual(a, b) { + if (a.byteLength != b.byteLength) { + return false; + } + let ui8b = new Uint8Array(b); + return new Uint8Array(a).every((val, i) => val === ui8b[i]); + } + + return arrayBuffersAreEqual(k1, k2); + } + + return false; +} + +function addPermission(permission, url) { + throw new Error("addPermission"); +} + +function removePermission(permission, url) { + throw new Error("removePermission"); +} + +function allowIndexedDB(url) { + throw new Error("allowIndexedDB"); +} + +function disallowIndexedDB(url) { + throw new Error("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 gc() { + Cu.forceGC(); + Cu.forceCC(); +} + +function scheduleGC() { + SpecialPowers.exactGC(continueToNextStep); +} + +function setTimeout(fun, timeout) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + var event = { + notify(timer) { + fun(); + }, + }; + timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +function initStorage() { + return Services.qms.init(); +} + +function initPersistentOrigin(principal) { + return Services.qms.initializePersistentOrigin(principal); +} + +function resetOrClearAllDatabases(callback, clear) { + if (!SpecialPowers.isMainProcess()) { + throw new Error("clearAllDatabases not implemented for child processes!"); + } + + const quotaPref = "dom.quotaManager.testing"; + + let oldPrefValue; + if (Services.prefs.prefHasUserValue(quotaPref)) { + oldPrefValue = SpecialPowers.getBoolPref(quotaPref); + } + + SpecialPowers.setBoolPref(quotaPref, true); + + let request; + + try { + if (clear) { + request = Services.qms.clear(); + } else { + request = Services.qms.reset(); + } + } catch (e) { + if (oldPrefValue !== undefined) { + SpecialPowers.setBoolPref(quotaPref, oldPrefValue); + } else { + SpecialPowers.clearUserPref(quotaPref); + } + throw e; + } + + request.callback = callback; + + return request; +} + +function resetAllDatabases(callback) { + return resetOrClearAllDatabases(callback, false); +} + +function clearAllDatabases(callback) { + return resetOrClearAllDatabases(callback, true); +} + +function installPackagedProfile(packageName) { + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + let currentDir = Services.dirsvc.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 = []; + for (let entry of zipReader.findEntries(null)) { + 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 profileDir = Services.dirsvc.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 file = idbEntries.nextFile; + 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 }); +} + +function isWasmSupported() { + let testingFunctions = Cu.getJSTestingFunctions(); + return testingFunctions.wasmIsSupported(); +} + +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(Blob.isInstance(blob1), true, "Instance of nsIDOMBlob"); + is(File.isInstance(blob1), File.isInstance(blob2), "Instance of DOM File"); + is(blob1.size, blob2.size, "Correct size"); + is(blob1.type, blob2.type, "Correct type"); + if (File.isInstance(blob2)) { + 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 verifyView(view1, view2) { + is(view1.byteLength, view2.byteLength, "Correct byteLength"); + verifyBuffers(view1, view2); + continueToNextStep(); +} + +function grabFileUsageAndContinueHandler(request) { + testGenerator.next(request.result.fileUsage); +} + +function getCurrentUsage(usageHandler) { + let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal + ); + Services.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 resetDataThreshold() { + info("Clearing data threshold pref"); + SpecialPowers.clearUserPref("dom.indexedDB.dataThreshold"); +} + +function setMaxSerializedMsgSize(aSize) { + info("Setting maximal size of a serialized message to " + aSize); + SpecialPowers.setIntPref("dom.indexedDB.maxSerializedMsgSize", aSize); +} + +function enablePreprocessing() { + info("Setting preprocessing pref"); + SpecialPowers.setBoolPref("dom.indexedDB.preprocessing", true); +} + +function resetPreprocessing() { + info("Clearing preprocessing pref"); + SpecialPowers.clearUserPref("dom.indexedDB.preprocessing"); +} + +function getSystemPrincipal() { + return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); +} + +function getPrincipal(url) { + let uri = Services.io.newURI(url); + return Services.scriptSecurityManager.createContentPrincipal(uri, {}); +} + +class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } +} + +async function requestFinished(request) { + await new Promise(function (resolve) { + request.callback = function () { + resolve(); + }; + }); + + if (request.resultCode !== Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} + +// TODO: Rename to openDBRequestSucceeded ? +function expectingSuccess(request) { + return new Promise(function (resolve, reject) { + request.onerror = function (event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onsuccess = function (event) { + resolve(event); + }; + request.onupgradeneeded = function (event) { + ok(false, "Got upgrade, but did not expect it!"); + reject(event); + }; + }); +} + +// TODO: Rename to openDBRequestUpgradeNeeded ? +function expectingUpgrade(request) { + return new Promise(function (resolve, reject) { + request.onerror = function (event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onupgradeneeded = function (event) { + resolve(event); + }; + request.onsuccess = function (event) { + ok(false, "Got success, but did not expect it!"); + reject(event); + }; + }); +} + +function requestSucceeded(request) { + return new Promise(function (resolve, reject) { + request.onerror = function (event) { + ok(false, "indexedDB error, '" + event.target.error.name + "'"); + reject(event); + }; + request.onsuccess = function (event) { + resolve(event); + }; + }); +} + +// Given a "/"-delimited path relative to the profile directory, +// return an nsIFile representing the path. This does not test +// for the existence of the file or parent directories. +// It is safe even on Windows where the directory separator is not "/", +// but make sure you're not passing in a "\"-delimited path. +function getRelativeFile(relativePath) { + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + + let file = profileDir.clone(); + relativePath.split("/").forEach(function (component) { + file.append(component); + }); + + return file; +} + +var SpecialPowers = { + isMainProcess() { + return ( + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); + }, + notifyObservers(subject, topic, data) { + Services.obs.notifyObservers(subject, topic, data); + }, + notifyObserversInParentProcess(subject, topic, data) { + if (subject) { + throw new Error("Can't send subject to another process!"); + } + return this.notifyObservers(subject, topic, data); + }, + getBoolPref(prefName) { + return Services.prefs.getBoolPref(prefName); + }, + setBoolPref(prefName, value) { + Services.prefs.setBoolPref(prefName, value); + }, + setIntPref(prefName, value) { + Services.prefs.setIntPref(prefName, value); + }, + clearUserPref(prefName) { + Services.prefs.clearUserPref(prefName); + }, + // Copied (and slightly adjusted) from testing/specialpowers/content/SpecialPowersAPI.jsm + exactGC(callback) { + let count = 0; + + function doPreciseGCandCC() { + function scheduledGCCallback() { + Cu.forceCC(); + + if (++count < 3) { + doPreciseGCandCC(); + } else { + callback(); + } + } + + Cu.schedulePreciseGC(scheduledGCCallback); + } + + doPreciseGCandCC(); + }, + + get Cc() { + return Cc; + }, + + get Ci() { + return Ci; + }, + + get Cu() { + return Cu; + }, + + // Based on SpecialPowersObserver.prototype.receiveMessage + createFiles(requests, callback) { + let filePaths = []; + if (!this._createdFiles) { + this._createdFiles = []; + } + let createdFiles = this._createdFiles; + let promises = []; + requests.forEach(function (request) { + const filePerms = 0o666; + let testFile = Services.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(); + } + promises.push( + File.createFromFileName(testFile.path, request.options).then(function ( + file + ) { + filePaths.push(file); + }) + ); + createdFiles.push(testFile); + }); + + Promise.all(promises).then(function () { + setTimeout(function () { + callback(filePaths); + }, 0); + }); + }, + + removeFiles() { + 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 0000000000..e1b02c2c1d --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-parent-process.ini @@ -0,0 +1,67 @@ +# 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 = +support-files = + bug1056939_profile.zip + idbSubdirUpgrade1_profile.zip + idbSubdirUpgrade2_profile.zip + mutableFileUpgrade_profile.zip + obsoleteOriginAttributes_profile.zip + orphaned_files_profile.zip + oldDirectories_profile.zip + GlobalObjectsChild.js + GlobalObjectsModule.sys.mjs + GlobalObjectsSandbox.js + metadata2Restore_profile.zip + metadataRestore_profile.zip + schema18upgrade_profile.zip + schema21upgrade_profile.zip + schema23upgrade_profile.zip + snappyUpgrade_profile.zip + storagePersistentUpgrade_profile.zip + wasm_get_values_profile.zip + xpcshell-shared.ini + +[include:xpcshell-shared.ini] + +[test_bad_origin_directory.js] +[test_obsoleteOriginAttributesUpgrade.js] +[test_orphaned_files.js] +[test_blob_file_backed.js] +[test_bug1056939.js] +[test_cleanup_transaction.js] +[test_database_close_without_onclose.js] +[test_database_onclose.js] +[test_file_copy_failure.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_marker_file.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_unexpectedDirectory.js] +[test_view_put_get_values.js] +[test_wasm_get_values.js] +[test_wasm_put_get_values.js] diff --git a/dom/indexedDB/test/unit/xpcshell-shared.ini b/dom/indexedDB/test/unit/xpcshell-shared.ini new file mode 100644 index 0000000000..4107d37c95 --- /dev/null +++ b/dom/indexedDB/test/unit/xpcshell-shared.ini @@ -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/. + +[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] +skip-if = + os == "win" && debug # Bug 1785080 +[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] +skip-if = (tsan || asan) # Uncatched OOM crashes, bug 1796753 +[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_put_get_values.js] +[test_put_get_values_autoIncrement.js] +skip-if = (verify && debug && (os == 'win')) +[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_upgrade_add_index.js] +[test_writer_starvation.js] |