summaryrefslogtreecommitdiffstats
path: root/dom/indexedDB/ActorsParent.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/indexedDB/ActorsParent.cpp
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/indexedDB/ActorsParent.cpp')
-rw-r--r--dom/indexedDB/ActorsParent.cpp20861
1 files changed, 20861 insertions, 0 deletions
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(&current)) &&
+ 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 = &params.commonParams();
+ break;
+ }
+
+ case FactoryRequestParams::TDeleteDatabaseRequestParams: {
+ const DeleteDatabaseRequestParams& params =
+ aParams.get_DeleteDatabaseRequestParams();
+ commonParams = &params.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 = [&quotaManager] {
+ 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, &quotaManager, 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, &quotaManager, 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,
+ [&params = 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,
+ [&params = 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