summaryrefslogtreecommitdiffstats
path: root/dom/localstorage/ActorsParent.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/localstorage/ActorsParent.cpp')
-rw-r--r--dom/localstorage/ActorsParent.cpp9014
1 files changed, 9014 insertions, 0 deletions
diff --git a/dom/localstorage/ActorsParent.cpp b/dom/localstorage/ActorsParent.cpp
new file mode 100644
index 0000000000..2554628d7f
--- /dev/null
+++ b/dom/localstorage/ActorsParent.cpp
@@ -0,0 +1,9014 @@
+/* -*- 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"
+
+// Local includes
+#include "LSInitializationTypes.h"
+#include "LSObject.h"
+#include "ReportInternalError.h"
+
+// Global includes
+#include <cinttypes>
+#include <cstdlib>
+#include <cstring>
+#include <new>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "mozIStorageAsyncConnection.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageFunction.h"
+#include "mozIStorageService.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageValueArray.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DebugOnly.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/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/Variant.h"
+#include "mozilla/dom/ClientManagerService.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/LSSnapshot.h"
+#include "mozilla/dom/LSValue.h"
+#include "mozilla/dom/LSWriteOptimizer.h"
+#include "mozilla/dom/LSWriteOptimizerImpl.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/PBackgroundLSDatabase.h"
+#include "mozilla/dom/PBackgroundLSDatabaseParent.h"
+#include "mozilla/dom/PBackgroundLSObserverParent.h"
+#include "mozilla/dom/PBackgroundLSRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSharedTypes.h"
+#include "mozilla/dom/PBackgroundLSSimpleRequestParent.h"
+#include "mozilla/dom/PBackgroundLSSnapshotParent.h"
+#include "mozilla/dom/SnappyUtils.h"
+#include "mozilla/dom/StorageDBUpdater.h"
+#include "mozilla/dom/StorageUtils.h"
+#include "mozilla/dom/ipc/IdType.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/FirstInitializationAttemptsImpl.h"
+#include "mozilla/dom/quota/OriginScope.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/StorageHelpers.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/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundParent.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/storage/Variant.h"
+#include "nsBaseHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsHashKeys.h"
+#include "nsIBinaryInputStream.h"
+#include "nsIBinaryOutputStream.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIEventTarget.h"
+#include "nsIFile.h"
+#include "nsIInputStream.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIOutputStream.h"
+#include "nsIRunnable.h"
+#include "nsISerialEventTarget.h"
+#include "nsISupports.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+#include "nsIVariant.h"
+#include "nsInterfaceHashtable.h"
+#include "nsLiteralString.h"
+#include "nsNetUtil.h"
+#include "nsPointerHashKeys.h"
+#include "nsPrintfCString.h"
+#include "nsRefPtrHashtable.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashSet.h"
+#include "nsTLiteralString.h"
+#include "nsTStringRepr.h"
+#include "nsThreadUtils.h"
+#include "nsVariant.h"
+#include "nsXPCOM.h"
+#include "nsXULAppAPI.h"
+#include "nscore.h"
+#include "prenv.h"
+#include "prtime.h"
+
+#define LS_LOG_TEST() MOZ_LOG_TEST(GetLocalStorageLogger(), LogLevel::Info)
+#define LS_LOG(_args) MOZ_LOG(GetLocalStorageLogger(), LogLevel::Info, _args)
+
+#if defined(MOZ_WIDGET_ANDROID)
+# define LS_MOBILE
+#endif
+
+namespace mozilla::dom {
+
+using namespace mozilla::dom::quota;
+using namespace mozilla::dom::StorageUtils;
+using namespace mozilla::ipc;
+
+namespace {
+
+struct ArchivedOriginInfo;
+class ArchivedOriginScope;
+class Connection;
+class ConnectionThread;
+class Database;
+class Observer;
+class PrepareDatastoreOp;
+class PreparedDatastore;
+class QuotaClient;
+class Snapshot;
+
+using ArchivedOriginHashtable =
+ nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo>;
+
+/*******************************************************************************
+ * Constants
+ ******************************************************************************/
+
+// Major schema version. Bump for almost everything.
+const uint32_t kMajorSchemaVersion = 5;
+
+// Minor schema version. Should almost always be 0 (maybe bump on release
+// branches if we have to).
+const uint32_t kMinorSchemaVersion = 0;
+
+// The schema version we store in the SQLite database is a (signed) 32-bit
+// integer. The major version is left-shifted 4 bits so the max value is
+// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF.
+static_assert(kMajorSchemaVersion <= 0xFFFFFFF,
+ "Major version needs to fit in 28 bits.");
+static_assert(kMinorSchemaVersion <= 0xF,
+ "Minor version needs to fit in 4 bits.");
+
+const int32_t kSQLiteSchemaVersion =
+ int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion);
+
+// 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 LS_MOBILE
+ 512;
+#else
+ 1024;
+#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 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 database name for LocalStorage data in a per-origin directory.
+ */
+constexpr auto kDataFileName = u"data.sqlite"_ns;
+
+/**
+ * The journal corresponding to kDataFileName. (We don't use WAL mode.)
+ * Currently only needed in QuotaClient::InitOrigin and only in DEBUG builds.
+ * See the corresponding comment in QuotaClient::InitOrigin.
+ */
+#ifdef DEBUG
+constexpr auto kJournalFileName = u"data.sqlite-journal"_ns;
+#endif
+
+/**
+ * This file contains the current usage of the LocalStorage database as defined
+ * by the mozLength totals of all keys and values for the database, which
+ * differs from the actual size on disk. We store this value in a separate
+ * file as a cache so that we can initialize the QuotaClient faster.
+ * In the future, this file will be eliminated and the information will be
+ * stored in PROFILE/storage.sqlite or similar QuotaManager-wide storage.
+ *
+ * The file contains a binary verification cookie (32-bits) followed by the
+ * actual usage (64-bits).
+ */
+constexpr auto kUsageFileName = u"usage"_ns;
+
+/**
+ * Following a QuotaManager idiom, this journal file's existence is a marker
+ * that the usage file was in the process of being updated and is currently
+ * invalid. This file is created prior to updating the usage file and only
+ * deleted after the usage file has been written and closed and any pending
+ * database transactions have been committed. Note that this idiom is expected
+ * to work if Gecko crashes in the middle of a write, but is not expected to be
+ * foolproof in the face of a system crash, as we do not explicitly attempt to
+ * fsync the directory containing the journal file.
+ *
+ * If the journal file is found to exist at origin initialization time, the
+ * usage will be re-computed from the current state of DATA_FILE_NAME.
+ */
+constexpr auto kUsageJournalFileName = u"usage-journal"_ns;
+
+static const uint32_t kUsageFileSize = 12;
+static const uint32_t kUsageFileCookie = 0x420a420a;
+
+/**
+ * How long between the first moment we know we have data to be written on a
+ * `Connection` and when we should actually perform the write. This helps
+ * limit disk churn under silly usage patterns and is historically consistent
+ * with the previous, legacy implementation.
+ *
+ * Note that flushing happens downstream of Snapshot checkpointing and its
+ * batch mechanism which helps avoid wasteful IPC in the case of silly content
+ * code.
+ */
+const uint32_t kFlushTimeoutMs = 5000;
+
+const bool kDefaultShadowWrites = false;
+const uint32_t kDefaultSnapshotPrefill = 16384;
+const uint32_t kDefaultSnapshotGradualPrefill = 4096;
+const bool kDefaultClientValidation = true;
+/**
+ * Should all mutations also be reflected in the "shadow" database, which is
+ * the legacy webappsstore.sqlite database. When this is enabled, users can
+ * downgrade their version of Firefox and/or otherwise fall back to the legacy
+ * implementation without loss of data. (Older versions of Firefox will
+ * recognize the presence of ls-archive.sqlite and purge it and the other
+ * LocalStorage directories so privacy is maintained.)
+ */
+const char kShadowWritesPref[] = "dom.storage.shadow_writes";
+/**
+ * Byte budget for sending data down to the LSSnapshot instance when it is first
+ * created. If there is less data than this (measured by tallying the string
+ * length of the keys and values), all data is sent, otherwise partial data is
+ * sent. See `Snapshot`.
+ */
+const char kSnapshotPrefillPref[] = "dom.storage.snapshot_prefill";
+/**
+ * When a specific value is requested by an LSSnapshot that is not already fully
+ * populated, gradual prefill is used. This preference specifies the number of
+ * bytes to be used to send values beyond the specific value that is requested.
+ * (The size of the explicitly requested value does not impact this preference.)
+ * Setting the value to 0 disables gradual prefill. Tests may set this value to
+ * -1 which is converted to INT_MAX in order to cause gradual prefill to send
+ * all values not previously sent.
+ */
+const char kSnapshotGradualPrefillPref[] =
+ "dom.storage.snapshot_gradual_prefill";
+
+const char kClientValidationPref[] = "dom.storage.client_validation";
+
+/**
+ * The amount of time a PreparedDatastore instance should stick around after a
+ * preload is triggered in order to give time for the page to use LocalStorage
+ * without triggering worst-case synchronous jank.
+ */
+const uint32_t kPreparedDatastoreTimeoutMs = 20000;
+
+/**
+ * Cold storage for LocalStorage data extracted from webappsstore.sqlite at
+ * LSNG first-run that has not yet been migrated to its own per-origin directory
+ * by use.
+ *
+ * In other words, at first run, LSNG copies the contents of webappsstore.sqlite
+ * into this database. As requests are made for that LocalStorage data, the
+ * contents are removed from this database and placed into per-origin QM
+ * storage. So the contents of this database are always old, unused
+ * LocalStorage data that we can potentially get rid of at some point in the
+ * future.
+ */
+#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite"
+/**
+ * The legacy LocalStorage database. Its contents are maintained as our
+ * "shadow" database so that LSNG can be disabled without loss of user data.
+ */
+#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite"
+
+// Shadow database Write Ahead Log's maximum size is 512KB
+const uint32_t kShadowMaxWALSize = 512 * 1024;
+
+bool IsOnGlobalConnectionThread();
+
+void AssertIsOnGlobalConnectionThread();
+
+/*******************************************************************************
+ * SQLite functions
+ ******************************************************************************/
+
+int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion,
+ uint32_t aMinorSchemaVersion) {
+ return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion);
+}
+
+nsCString GetArchivedOriginHashKey(const nsACString& aOriginSuffix,
+ const nsACString& aOriginNoSuffix) {
+ return aOriginSuffix + ":"_ns + aOriginNoSuffix;
+}
+
+nsresult CreateDataTable(mozIStorageConnection* aConnection) {
+ return aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE data"
+ "( key TEXT PRIMARY KEY"
+ ", utf16_length INTEGER NOT NULL"
+ ", conversion_type INTEGER NOT NULL"
+ ", compression_type INTEGER NOT NULL"
+ ", last_access_time INTEGER NOT NULL DEFAULT 0"
+ ", value BLOB NOT NULL"
+ ");"_ns);
+}
+
+nsresult CreateTables(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aConnection);
+
+ // Table `database`
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE database"
+ "( origin TEXT NOT NULL"
+ ", usage INTEGER NOT NULL DEFAULT 0"
+ ", last_vacuum_time INTEGER NOT NULL DEFAULT 0"
+ ", last_analyze_time INTEGER NOT NULL DEFAULT 0"
+ ", last_vacuum_size INTEGER NOT NULL DEFAULT 0"
+ ");"_ns)));
+
+ // Table `data`
+ QM_TRY(MOZ_TO_RESULT(CreateDataTable(aConnection)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kSQLiteSchemaVersion)));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE database ADD COLUMN usage INTEGER NOT NULL DEFAULT 0;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "UPDATE database "
+ "SET usage = (SELECT total(utf16Length(key) + utf16Length(value)) "
+ "FROM data);"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0))));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom2_0To3_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE data ADD COLUMN utf16Length INTEGER NOT NULL DEFAULT 0;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "UPDATE data SET utf16Length = utf16Length(value);"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(3, 0))));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom3_0To4_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(4, 0))));
+
+ return NS_OK;
+}
+
+nsresult UpgradeSchemaFrom4_0To5_0(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ // Recreate data table in new format following steps at
+ // https://www.sqlite.org/lang_altertable.html
+ // section "Making Other Kinds Of Table Schema Changes"
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE migrated_data"
+ "( key TEXT PRIMARY KEY"
+ ", utf16_length INTEGER NOT NULL"
+ ", conversion_type INTEGER NOT NULL"
+ ", compression_type INTEGER NOT NULL"
+ ", last_access_time INTEGER NOT NULL DEFAULT 0"
+ ", value BLOB NOT NULL"
+ ");"_ns)));
+
+ // Reinsert old data, all legacy data is UTF8
+ static_assert(1u ==
+ static_cast<uint8_t>(LSValue::ConversionType::UTF16_UTF8));
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "INSERT INTO migrated_data (key, utf16_length, conversion_type, "
+ "compression_type, last_access_time, value) "
+ "SELECT key, utf16Length, 1, compressed, lastAccessTime, value "
+ "FROM data;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL("DROP TABLE data;"_ns)));
+
+ // Rename to data
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "ALTER TABLE migrated_data RENAME TO data;"_ns)));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeSchemaVersion(5, 0))));
+
+ return NS_OK;
+}
+
+nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("PRAGMA synchronous = FULL;"_ns)));
+
+#ifndef LS_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 // LS_MOBILE
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateStorageConnection(
+ nsIFile& aDBFile, nsIFile& aUsageFile, const nsACString& aOrigin) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+
+ // XXX Common logic should be refactored out of this method and
+ // cache::DBAction::OpenDBConnection, and maybe other similar functions.
+
+ 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, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>,
+ storageService, OpenDatabase, &aDBFile,
+ mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection)));
+
+ // Check to make sure that the database schema is correct.
+ // XXX Try to make schemaVersion const.
+ QM_TRY_UNWRAP(int32_t schemaVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(connection, GetSchemaVersion));
+
+ QM_TRY(OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_FAILURE));
+
+ if (schemaVersion != kSQLiteSchemaVersion) {
+ const bool newDatabase = !schemaVersion;
+
+ if (newDatabase) {
+ // Set the page size first.
+ if (kSQLitePageSizeOverride) {
+ QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(nsPrintfCString(
+ "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride))));
+ }
+
+ // We have to set the auto_vacuum mode before opening a transaction.
+ QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL(
+#ifdef LS_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
+ )));
+ }
+
+ bool vacuumNeeded = false;
+
+ if (newDatabase) {
+ mozStorageTransaction transaction(
+ connection,
+ /* aCommitOnComplete */ false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ 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
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "INSERT INTO database (origin) VALUES (:origin)"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("origin"_ns, aOrigin)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+ } else {
+ // This logic needs to change next time we change the schema!
+ static_assert(kSQLiteSchemaVersion == int32_t((5 << 4) + 0),
+ "Upgrade function needed due to schema version increase.");
+
+ while (schemaVersion != kSQLiteSchemaVersion) {
+ mozStorageTransaction transaction(
+ connection,
+ /* aCommitOnComplete */ false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ if (schemaVersion == MakeSchemaVersion(1, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom1_0To2_0(connection)));
+ } else if (schemaVersion == MakeSchemaVersion(2, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom2_0To3_0(connection)));
+ } else if (schemaVersion == MakeSchemaVersion(3, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom3_0To4_0(connection)));
+ } else if (schemaVersion == MakeSchemaVersion(4, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeSchemaFrom4_0To5_0(connection)));
+ vacuumNeeded = true;
+ } else {
+ LS_WARNING(
+ "Unable to open LocalStorage database, no upgrade path is "
+ "available!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ QM_TRY_UNWRAP(schemaVersion, MOZ_TO_RESULT_INVOKE_MEMBER(
+ connection, GetSchemaVersion));
+ }
+
+ MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
+ }
+
+ if (vacuumNeeded) {
+ QM_TRY(MOZ_TO_RESULT(connection->ExecuteSimpleSQL("VACUUM;"_ns)));
+ }
+
+ if (newDatabase) {
+ // Windows caches the file size, let's force it to stat the file again.
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists));
+ Unused << exists;
+
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, GetFileSize));
+
+ MOZ_ASSERT(fileSize > 0);
+
+ const PRTime vacuumTime = PR_Now();
+ MOZ_ASSERT(vacuumTime);
+
+ 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->BindInt64ByName("time"_ns, vacuumTime)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(vacuumTimeStmt->BindInt64ByName("size"_ns, fileSize)));
+
+ QM_TRY(MOZ_TO_RESULT(vacuumTimeStmt->Execute()));
+ }
+ }
+
+ return connection;
+}
+
+template <typename CorruptedFileHandler>
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+CreateStorageConnectionWithRecovery(
+ nsIFile& aDBFile, nsIFile& aUsageFile, const nsACString& aOrigin,
+ CorruptedFileHandler&& aCorruptedFileHandler) {
+ QM_TRY_RETURN(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ CreateStorageConnection(aDBFile, aUsageFile, aOrigin),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ([&aDBFile, &aUsageFile, &aOrigin,
+ &aCorruptedFileHandler](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ // Remove the usage file first (it might not exist at all due
+ // to corrupted state, which is ignored here).
+
+ // Usually we only use QM_OR_ELSE_LOG_VERBOSE(_IF) with Remove and
+ // NS_ERROR_FILE_NOT_FOUND check, but we're already in the rare case
+ // of corruption here, so the use of QM_OR_ELSE_WARN_IF is ok here.
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(aUsageFile.Remove(false)),
+ // Predicate.
+ ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }),
+ // Fallback.
+ ErrToDefaultOk<>));
+
+ // Call the corrupted file handler before trying to remove the
+ // database file, which might fail.
+ aCorruptedFileHandler();
+
+ // Nuke the database file.
+ QM_TRY(MOZ_TO_RESULT(aDBFile.Remove(false)));
+
+ QM_TRY_RETURN(CreateStorageConnection(aDBFile, aUsageFile, aOrigin));
+ })));
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetStorageConnection(
+ const nsAString& aDatabaseFilePath) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
+ MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, u".sqlite"_ns));
+
+ QM_TRY_INSPECT(const auto& databaseFile, QM_NewLocalFile(aDatabaseFilePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(databaseFile, Exists));
+
+ QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_UNWRAP(auto connection,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenDatabase,
+ databaseFile, mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(SetDefaultPragmas(connection)));
+
+ return connection;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetArchiveFile(
+ const nsAString& aStoragePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aStoragePath));
+
+ QM_TRY(MOZ_TO_RESULT(
+ archiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))));
+
+ return archiveFile;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+CreateArchiveStorageConnection(const nsAString& aStoragePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aStoragePath.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& archiveFile, GetArchiveFile(aStoragePath));
+
+ // QuotaManager ensures this file always exists.
+ DebugOnly<bool> exists;
+ MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists)));
+ MOZ_ASSERT(exists);
+
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(archiveFile, IsDirectory));
+
+ if (isDirectory) {
+ LS_WARNING("ls-archive is not a file!");
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+
+ QM_TRY_INSPECT(const auto& ss,
+ 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.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ archiveFile, mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback. Don't throw an error, leave a corrupted ls-archive
+ // database as it is.
+ ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
+
+ if (connection) {
+ const nsresult rv = StorageDBUpdater::Update(connection);
+ if (NS_FAILED(rv)) {
+ // Don't throw an error, leave a non-updateable ls-archive database as
+ // it is.
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+ }
+
+ return connection;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetShadowFile(const nsAString& aBasePath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aBasePath));
+
+ QM_TRY(MOZ_TO_RESULT(
+ archiveFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME))));
+
+ return archiveFile;
+}
+
+nsresult SetShadowJournalMode(mozIStorageConnection* aConnection) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aConnection);
+
+ // 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(nsAutoCString, *stmt,
+ GetUTF8String, 0));
+
+ if (journalMode.Equals(journalModeWAL)) {
+ // WAL mode successfully enabled. Set limits on its size here.
+
+ // Set the threshold for auto-checkpointing the WAL. We don't want giant
+ // logs slowing down us.
+ QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement(
+ *aConnection, "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);
+
+ // Note there is a default journal_size_limit set by mozStorage.
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "PRAGMA wal_autocheckpoint = "_ns +
+ IntToCString(static_cast<int32_t>(kShadowMaxWALSize / pageSize)))));
+ } else {
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns)));
+ }
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateShadowStorageConnection(
+ const nsAString& aBasePath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+ QM_TRY_INSPECT(const auto& ss,
+ 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.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ shadowFile, mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ([&shadowFile, &ss](const nsresult rv)
+ -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false)));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ shadowFile, mozIStorageService::CONNECTION_DEFAULT));
+ })));
+
+ QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection)));
+
+ // XXX Depending on whether the *first* call to OpenUnsharedDatabase above
+ // failed, we (a) might or (b) might not be dealing with a fresh database
+ // here. This is confusing, since in a failure of case (a) we would do the
+ // same thing again. Probably, the control flow should be changed here so that
+ // it's clear we only delete & create a fresh database once. If we still have
+ // a failure then, we better give up. Or, if we really want to handle that,
+ // the number of 2 retries seems arbitrary, and we should better do this in
+ // some loop until a maximum number of retries is reached.
+ //
+ // Compare this with QuotaManager::CreateLocalStorageArchiveConnection, which
+ // actually tracks if the file was removed before, but it's also more
+ // complicated than it should be. Maybe these two methods can be merged (which
+ // would mean that a parameter must be added that indicates whether it's
+ // handling the shadow file or not).
+ QM_TRY(QM_OR_ELSE_WARN(
+ // Expression.
+ MOZ_TO_RESULT(StorageDBUpdater::Update(connection)),
+ // Fallback.
+ ([&connection, &shadowFile, &ss](const nsresult) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+ QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false)));
+
+ QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, shadowFile,
+ mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(SetShadowJournalMode(connection)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection)));
+
+ return Ok{};
+ })));
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetShadowStorageConnection(
+ const nsAString& aBasePath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists));
+
+ QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, shadowFile,
+ mozIStorageService::CONNECTION_DEFAULT));
+}
+
+nsresult AttachShadowDatabase(const nsAString& aBasePath,
+ mozIStorageConnection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(!aBasePath.IsEmpty());
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(shadowFile, Exists));
+
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, shadowFile, GetPath));
+
+ QM_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection,
+ CreateStatement, "ATTACH DATABASE :path AS shadow;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindStringByName("path"_ns, path)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+}
+
+nsresult DetachShadowDatabase(mozIStorageConnection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("DETACH DATABASE shadow"_ns)));
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetUsageFile(
+ const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto usageFile, QM_NewLocalFile(aDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(usageFile->Append(kUsageFileName)));
+
+ return usageFile;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetUsageJournalFile(
+ const nsAString& aDirectoryPath) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto usageJournalFile, QM_NewLocalFile(aDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Append(kUsageJournalFileName)));
+
+ return usageJournalFile;
+}
+
+// Checks if aFile exists and is a file. Returns true if it exists and is a
+// file, false if it doesn't exist, and an error if it exists but isn't a file.
+Result<bool, nsresult> ExistsAsFile(nsIFile& aFile) {
+ enum class ExistsAsFileResult { DoesNotExist, IsDirectory, IsFile };
+
+ // This is an optimization to check both properties in one OS case, rather
+ // than calling Exists first, and then IsDirectory. IsDirectory also checks
+ // if the path exists. 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.
+ QM_TRY_INSPECT(
+ const auto& res,
+ QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsDirectory)
+ .map([](const bool isDirectory) {
+ return isDirectory ? ExistsAsFileResult::IsDirectory
+ : ExistsAsFileResult::IsFile;
+ }),
+ // Predicate.
+ ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }),
+ // Fallback.
+ ErrToOk<ExistsAsFileResult::DoesNotExist>));
+
+ QM_TRY(OkIf(res != ExistsAsFileResult::IsDirectory), Err(NS_ERROR_FAILURE));
+
+ return res == ExistsAsFileResult::IsFile;
+}
+
+nsresult UpdateUsageFile(nsIFile* aUsageFile, nsIFile* aUsageJournalFile,
+ int64_t aUsage) {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aUsageFile);
+ MOZ_ASSERT(aUsageJournalFile);
+ MOZ_ASSERT(aUsage >= 0);
+
+ QM_TRY_INSPECT(const bool& usageJournalFileExists,
+ ExistsAsFile(*aUsageJournalFile));
+ if (!usageJournalFileExists) {
+ QM_TRY(MOZ_TO_RESULT(
+ aUsageJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
+ }
+
+ QM_TRY_INSPECT(const auto& stream, NS_NewLocalFileOutputStream(aUsageFile));
+
+ nsCOMPtr<nsIBinaryOutputStream> binaryStream =
+ NS_NewObjectOutputStream(stream);
+
+ QM_TRY(MOZ_TO_RESULT(binaryStream->Write32(kUsageFileCookie)));
+
+ QM_TRY(MOZ_TO_RESULT(binaryStream->Write64(aUsage)));
+
+#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
+ QM_TRY(MOZ_TO_RESULT(stream->Flush()));
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(stream->Close()));
+
+ return NS_OK;
+}
+
+Result<UsageInfo, nsresult> LoadUsageFile(nsIFile& aUsageFile) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const int64_t& fileSize,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aUsageFile, GetFileSize));
+
+ QM_TRY(OkIf(fileSize == kUsageFileSize), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(&aUsageFile));
+
+ QM_TRY_INSPECT(const auto& bufferedStream,
+ NS_NewBufferedInputStream(stream.forget(), 16));
+
+ const nsCOMPtr<nsIBinaryInputStream> binaryStream =
+ NS_NewObjectInputStream(bufferedStream);
+
+ QM_TRY_INSPECT(const uint32_t& cookie,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
+
+ QM_TRY(OkIf(cookie == kUsageFileCookie), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_INSPECT(const uint64_t& usage,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
+
+ return UsageInfo{DatabaseUsageType(Some(usage))};
+}
+
+/*******************************************************************************
+ * Non-actor class declarations
+ ******************************************************************************/
+
+/**
+ * Coalescing manipulation queue used by `Datastore`. Used by `Datastore` to
+ * update `Datastore::mOrderedItems` efficiently/for code simplification.
+ * (Datastore does not actually depend on the coalescing, as mutations are
+ * applied atomically when a Snapshot Checkpoints, and with `Datastore::mValues`
+ * being updated at the same time the mutations are applied to Datastore's
+ * mWriteOptimizer.)
+ */
+class DatastoreWriteOptimizer final : public LSWriteOptimizer<LSValue> {
+ public:
+ void ApplyAndReset(nsTArray<LSItemInfo>& aOrderedItems);
+};
+
+/**
+ * Coalescing manipulation queue used by `Connection`. Used by `Connection` to
+ * buffer and coalesce manipulations applied to the Datastore in batches by
+ * Snapshot Checkpointing until flushed to disk.
+ */
+class ConnectionWriteOptimizer final : public LSWriteOptimizer<LSValue> {
+ public:
+ // Returns the usage as the success value.
+ Result<int64_t, nsresult> Perform(Connection* aConnection,
+ bool aShadowWrites);
+
+ private:
+ /**
+ * Handlers for specific mutations. Each method knows how to `Perform` the
+ * manipulation against a `Connection` and the "shadow" database (legacy
+ * webappsstore.sqlite database that exists so LSNG can be disabled/safely
+ * downgraded from.)
+ */
+ nsresult PerformInsertOrUpdate(Connection* aConnection, bool aShadowWrites,
+ const nsAString& aKey, const LSValue& aValue);
+
+ nsresult PerformDelete(Connection* aConnection, bool aShadowWrites,
+ const nsAString& aKey);
+
+ nsresult PerformTruncate(Connection* aConnection, bool aShadowWrites);
+};
+
+class DatastoreOperationBase : public Runnable {
+ nsCOMPtr<nsIEventTarget> mOwningEventTarget;
+ nsresult mResultCode;
+ Atomic<bool> mMayProceedOnNonOwningThread;
+ bool mMayProceed;
+
+ public:
+ nsIEventTarget* OwningEventTarget() const {
+ MOZ_ASSERT(mOwningEventTarget);
+
+ return mOwningEventTarget;
+ }
+
+ bool IsOnOwningThread() const {
+ MOZ_ASSERT(mOwningEventTarget);
+
+ bool current;
+ return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) &&
+ current;
+ }
+
+ void AssertIsOnOwningThread() const {
+ MOZ_ASSERT(IsOnBackgroundThread());
+ MOZ_ASSERT(IsOnOwningThread());
+ }
+
+ nsresult ResultCode() const { return mResultCode; }
+
+ void SetFailureCode(nsresult aErrorCode) {
+ MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
+ MOZ_ASSERT(NS_FAILED(aErrorCode));
+
+ mResultCode = aErrorCode;
+ }
+
+ void MaybeSetFailureCode(nsresult aErrorCode) {
+ MOZ_ASSERT(NS_FAILED(aErrorCode));
+
+ if (NS_SUCCEEDED(mResultCode)) {
+ mResultCode = aErrorCode;
+ }
+ }
+
+ void NoteComplete() {
+ AssertIsOnOwningThread();
+
+ mMayProceed = false;
+ mMayProceedOnNonOwningThread = false;
+ }
+
+ bool MayProceed() const {
+ AssertIsOnOwningThread();
+
+ return mMayProceed;
+ }
+
+ // May be called on any thread, but you should call MayProceed() if you know
+ // you're on the background thread because it is slightly faster.
+ bool MayProceedOnNonOwningThread() const {
+ return mMayProceedOnNonOwningThread;
+ }
+
+ protected:
+ DatastoreOperationBase()
+ : Runnable("dom::DatastoreOperationBase"),
+ mOwningEventTarget(GetCurrentSerialEventTarget()),
+ mResultCode(NS_OK),
+ mMayProceedOnNonOwningThread(true),
+ mMayProceed(true) {}
+
+ ~DatastoreOperationBase() override { MOZ_ASSERT(!mMayProceed); }
+};
+
+class ConnectionDatastoreOperationBase : public DatastoreOperationBase {
+ protected:
+ RefPtr<Connection> mConnection;
+ /**
+ * This boolean flag is used by the CloseOp to avoid creating empty databases.
+ */
+ const bool mEnsureStorageConnection;
+
+ public:
+ // 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:
+ ConnectionDatastoreOperationBase(Connection* aConnection,
+ bool aEnsureStorageConnection = true);
+
+ ~ConnectionDatastoreOperationBase();
+
+ // Must be overridden in subclasses. Called on the target thread to allow the
+ // subclass to perform necessary datastore operations. A successful return
+ // value will trigger an OnSuccess callback on the background thread while
+ // while a failure value will trigger an OnFailure callback.
+ virtual nsresult DoDatastoreWork() = 0;
+
+ // Methods that subclasses may implement.
+ virtual void OnSuccess();
+
+ virtual void OnFailure(nsresult aResultCode);
+
+ private:
+ void RunOnConnectionThread();
+
+ void RunOnOwningThread();
+
+ // Not to be overridden by subclasses.
+ NS_DECL_NSIRUNNABLE
+};
+
+class Connection final : public CachingDatabaseConnection {
+ friend class ConnectionThread;
+
+ class InitTemporaryOriginHelper;
+
+ class FlushOp;
+ class CloseOp;
+
+ RefPtr<ConnectionThread> mConnectionThread;
+ RefPtr<QuotaClient> mQuotaClient;
+ nsCOMPtr<nsITimer> mFlushTimer;
+ UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
+ ConnectionWriteOptimizer mWriteOptimizer;
+ // XXX Consider changing this to ClientMetadata.
+ const OriginMetadata mOriginMetadata;
+ nsString mDirectoryPath;
+ /**
+ * Propagated from PrepareDatastoreOp. PrepareDatastoreOp may defer the
+ * creation of the localstorage client directory and database on the
+ * QuotaManager IO thread in its DatabaseWork method to
+ * Connection::EnsureStorageConnection, in which case the method needs to know
+ * it is responsible for taking those actions (without redundantly performing
+ * the existence checks).
+ */
+ const bool mDatabaseWasNotAvailable;
+ bool mHasCreatedDatabase;
+ bool mFlushScheduled;
+#ifdef DEBUG
+ bool mInUpdateBatch;
+ bool mFinished;
+#endif
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Connection)
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(Connection); }
+
+ QuotaClient* GetQuotaClient() const {
+ MOZ_ASSERT(mQuotaClient);
+
+ return mQuotaClient;
+ }
+
+ ArchivedOriginScope* GetArchivedOriginScope() const {
+ return mArchivedOriginScope.get();
+ }
+
+ const nsCString& Origin() const { return mOriginMetadata.mOrigin; }
+
+ const nsString& DirectoryPath() const { return mDirectoryPath; }
+
+ void GetFinishInfo(bool& aDatabaseWasNotAvailable,
+ bool& aHasCreatedDatabase) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mFinished);
+
+ aDatabaseWasNotAvailable = mDatabaseWasNotAvailable;
+ aHasCreatedDatabase = mHasCreatedDatabase;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Methods which can only be called on the owning thread.
+
+ // This method is used to asynchronously execute a connection datastore
+ // operation on the connection thread.
+ void Dispatch(ConnectionDatastoreOperationBase* aOp);
+
+ // This method is used to asynchronously close the storage connection on the
+ // connection thread.
+ void Close(nsIRunnable* aCallback);
+
+ void SetItem(const nsString& aKey, const LSValue& aValue, int64_t aDelta,
+ bool aIsNewItem);
+
+ void RemoveItem(const nsString& aKey, int64_t aDelta);
+
+ void Clear(int64_t aDelta);
+
+ void BeginUpdateBatch();
+
+ void EndUpdateBatch();
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Methods which can only be called on the connection thread.
+
+ nsresult EnsureStorageConnection();
+
+ mozIStorageConnection* StorageConnection() const {
+ AssertIsOnGlobalConnectionThread();
+
+ return &MutableStorageConnection();
+ }
+
+ void CloseStorageConnection();
+
+ nsresult BeginWriteTransaction();
+
+ nsresult CommitWriteTransaction();
+
+ nsresult RollbackWriteTransaction();
+
+ private:
+ // Only created by ConnectionThread.
+ Connection(ConnectionThread* aConnectionThread,
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable);
+
+ ~Connection();
+
+ void ScheduleFlush();
+
+ void Flush();
+
+ static void FlushTimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+/**
+ * Helper to invoke EnsureTemporaryOriginIsInitialized on the QuotaManager IO
+ * thread from the LocalStorage connection thread when creating a database
+ * connection on demand. This is necessary because we attempt to defer the
+ * creation of the origin directory and the database until absolutely needed,
+ * but the directory creation and origin initialization must happen on the QM
+ * IO thread for invariant reasons. (We can't just use a mutex because there
+ * could be logic on the IO thread that also wants to deal with the same
+ * origin, so we need to queue a runnable and wait our turn.)
+ */
+class Connection::InitTemporaryOriginHelper final : public Runnable {
+ mozilla::Monitor mMonitor MOZ_UNANNOTATED;
+ const OriginMetadata mOriginMetadata;
+ nsString mOriginDirectoryPath;
+ nsresult mIOThreadResultCode;
+ bool mWaiting;
+
+ public:
+ explicit InitTemporaryOriginHelper(const OriginMetadata& aOriginMetadata)
+ : Runnable("dom::localstorage::Connection::InitTemporaryOriginHelper"),
+ mMonitor("InitTemporaryOriginHelper::mMonitor"),
+ mOriginMetadata(aOriginMetadata),
+ mIOThreadResultCode(NS_OK),
+ mWaiting(true) {
+ AssertIsOnGlobalConnectionThread();
+ }
+
+ Result<nsString, nsresult> BlockAndReturnOriginDirectoryPath();
+
+ private:
+ ~InitTemporaryOriginHelper() = default;
+
+ nsresult RunOnIOThread();
+
+ NS_DECL_NSIRUNNABLE
+};
+
+class Connection::FlushOp final : public ConnectionDatastoreOperationBase {
+ ConnectionWriteOptimizer mWriteOptimizer;
+ bool mShadowWrites;
+
+ public:
+ FlushOp(Connection* aConnection, ConnectionWriteOptimizer&& aWriteOptimizer);
+
+ private:
+ nsresult DoDatastoreWork() override;
+
+ void Cleanup() override;
+};
+
+class Connection::CloseOp final : public ConnectionDatastoreOperationBase {
+ nsCOMPtr<nsIRunnable> mCallback;
+
+ public:
+ CloseOp(Connection* aConnection, nsIRunnable* aCallback)
+ : ConnectionDatastoreOperationBase(aConnection,
+ /* aEnsureStorageConnection */ false),
+ mCallback(aCallback) {}
+
+ private:
+ nsresult DoDatastoreWork() override;
+
+ void Cleanup() override;
+};
+
+class ConnectionThread final {
+ friend class Connection;
+
+ nsCOMPtr<nsIThread> mThread;
+ nsRefPtrHashtable<nsCStringHashKey, Connection> mConnections;
+
+ public:
+ ConnectionThread();
+
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(ConnectionThread);
+ }
+
+ bool IsOnConnectionThread();
+
+ void AssertIsOnConnectionThread();
+
+ already_AddRefed<Connection> CreateConnection(
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable);
+
+ void Shutdown();
+
+ NS_INLINE_DECL_REFCOUNTING(ConnectionThread)
+
+ private:
+ ~ConnectionThread();
+};
+
+/**
+ * Canonical state of Storage for an origin, containing all keys and their
+ * values in the parent process. Specifically, this is the state that will
+ * be handed out to freshly created Snapshots and that will be persisted to disk
+ * when the Connection's flush completes. State is mutated in batches as
+ * Snapshot instances Checkpoint their mutations locally accumulated in the
+ * child LSSnapshots.
+ */
+class Datastore final
+ : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ RefPtr<DirectoryLock> mDirectoryLock;
+ RefPtr<Connection> mConnection;
+ RefPtr<QuotaObject> mQuotaObject;
+ nsCOMPtr<nsIRunnable> mCompleteCallback;
+ /**
+ * PrepareDatastoreOps register themselves with the Datastore at
+ * and unregister in PrepareDatastoreOp::Cleanup.
+ */
+ nsTHashSet<PrepareDatastoreOp*> mPrepareDatastoreOps;
+ /**
+ * PreparedDatastore instances register themselves with their associated
+ * Datastore at construction time and unregister at destruction time. They
+ * hang around for kPreparedDatastoreTimeoutMs in order to keep the Datastore
+ * from closing itself via MaybeClose(), thereby giving the document enough
+ * time to load and access LocalStorage.
+ */
+ nsTHashSet<PreparedDatastore*> mPreparedDatastores;
+ /**
+ * A database is live (and in this hashtable) if it has a live LSDatabase
+ * actor. There is at most one Database per origin per content process. Each
+ * Database corresponds to an LSDatabase in its associated content process.
+ */
+ nsTHashSet<Database*> mDatabases;
+ /**
+ * A database is active if it has a non-null `mSnapshot`. As long as there
+ * are any active databases final deltas can't be calculated and
+ * `UpdateUsage()` can't be invoked.
+ */
+ nsTHashSet<Database*> mActiveDatabases;
+ /**
+ * Non-authoritative hashtable representation of mOrderedItems for efficient
+ * lookup.
+ */
+ nsTHashMap<nsStringHashKey, LSValue> mValues;
+ /**
+ * The authoritative ordered state of the Datastore; mValue also exists as an
+ * unordered hashtable for efficient lookup.
+ */
+ nsTArray<LSItemInfo> mOrderedItems;
+ nsTArray<int64_t> mPendingUsageDeltas;
+ DatastoreWriteOptimizer mWriteOptimizer;
+ const OriginMetadata mOriginMetadata;
+ const uint32_t mPrivateBrowsingId;
+ int64_t mUsage;
+ int64_t mUpdateBatchUsage;
+ int64_t mSizeOfKeys;
+ int64_t mSizeOfItems;
+ bool mClosed;
+ bool mInUpdateBatch;
+ bool mHasLivePrivateDatastore;
+
+ public:
+ // Created by PrepareDatastoreOp.
+ Datastore(const OriginMetadata& aOriginMetadata, uint32_t aPrivateBrowsingId,
+ int64_t aUsage, int64_t aSizeOfKeys, int64_t aSizeOfItems,
+ RefPtr<DirectoryLock>&& aDirectoryLock,
+ RefPtr<Connection>&& aConnection,
+ RefPtr<QuotaObject>&& aQuotaObject,
+ nsTHashMap<nsStringHashKey, LSValue>& aValues,
+ nsTArray<LSItemInfo>&& aOrderedItems);
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ const nsCString& Origin() const { return mOriginMetadata.mOrigin; }
+
+ uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; }
+
+ bool IsPersistent() const {
+ // Private-browsing is forbidden from touching disk, but
+ // StorageAccess::eSessionScoped is allowed to touch disk because
+ // QuotaManager's storage for such origins is wiped at shutdown.
+ return mPrivateBrowsingId == 0;
+ }
+
+ void Close();
+
+ bool IsClosed() const {
+ AssertIsOnBackgroundThread();
+
+ return mClosed;
+ }
+
+ void WaitForConnectionToComplete(nsIRunnable* aCallback);
+
+ void NoteLivePrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
+
+ void NoteFinishedPrepareDatastoreOp(PrepareDatastoreOp* aPrepareDatastoreOp);
+
+ void NoteLivePrivateDatastore();
+
+ void NoteFinishedPrivateDatastore();
+
+ void NoteLivePreparedDatastore(PreparedDatastore* aPreparedDatastore);
+
+ void NoteFinishedPreparedDatastore(PreparedDatastore* aPreparedDatastore);
+
+ bool HasOtherProcessDatabases(Database* aDatabase);
+
+ void NoteLiveDatabase(Database* aDatabase);
+
+ void NoteFinishedDatabase(Database* aDatabase);
+
+ void NoteActiveDatabase(Database* aDatabase);
+
+ void NoteInactiveDatabase(Database* aDatabase);
+
+ void GetSnapshotLoadInfo(const nsAString& aKey, bool& aAddKeyToUnknownItems,
+ nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTArray<LSItemInfo>& aItemInfos,
+ uint32_t& aNextLoadIndex,
+ LSSnapshot::LoadState& aLoadState);
+
+ uint32_t GetLength() const { return mValues.Count(); }
+
+ const nsTArray<LSItemInfo>& GetOrderedItems() const { return mOrderedItems; }
+
+ void GetItem(const nsAString& aKey, LSValue& aValue) const;
+
+ void GetKeys(nsTArray<nsString>& aKeys) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Mutation Methods
+ //
+ // These are only called during Snapshot::Checkpoint
+
+ /**
+ * Used by Snapshot::Checkpoint to set a key/value pair as part of an
+ * explicit batch.
+ */
+ void SetItem(Database* aDatabase, const nsString& aKey,
+ const LSValue& aValue);
+
+ void RemoveItem(Database* aDatabase, const nsString& aKey);
+
+ void Clear(Database* aDatabase);
+
+ void BeginUpdateBatch(int64_t aSnapshotUsage);
+
+ int64_t EndUpdateBatch(int64_t aSnapshotPeakUsage);
+
+ int64_t GetUsage() const { return mUsage; }
+
+ int64_t AttemptToUpdateUsage(int64_t aMinSize, bool aInitial);
+
+ bool HasOtherProcessObservers(Database* aDatabase);
+
+ void NotifyOtherProcessObservers(Database* aDatabase,
+ const nsString& aDocumentURI,
+ const nsString& aKey,
+ const LSValue& aOldValue,
+ const LSValue& aNewValue);
+
+ void NoteChangedObserverArray(const nsTArray<NotNull<Observer*>>& aObservers);
+
+ void Stringify(nsACString& aResult) const;
+
+ NS_INLINE_DECL_REFCOUNTING(Datastore)
+
+ private:
+ // Reference counted.
+ ~Datastore();
+
+ bool UpdateUsage(int64_t aDelta);
+
+ void MaybeClose();
+
+ void ConnectionClosedCallback();
+
+ void CleanupMetadata();
+
+ void NotifySnapshots(Database* aDatabase, const nsAString& aKey,
+ const LSValue& aOldValue, bool aAffectsOrder);
+
+ void NoteChangedDatabaseMap();
+};
+
+class PrivateDatastore {
+ const NotNull<RefPtr<Datastore>> mDatastore;
+
+ public:
+ explicit PrivateDatastore(MovingNotNull<RefPtr<Datastore>> aDatastore)
+ : mDatastore(std::move(aDatastore)) {
+ AssertIsOnBackgroundThread();
+
+ mDatastore->NoteLivePrivateDatastore();
+ }
+
+ ~PrivateDatastore() { mDatastore->NoteFinishedPrivateDatastore(); }
+
+ const Datastore& DatastoreRef() const {
+ AssertIsOnBackgroundThread();
+
+ return *mDatastore;
+ }
+};
+
+class PreparedDatastore {
+ RefPtr<Datastore> mDatastore;
+ nsCOMPtr<nsITimer> mTimer;
+ const Maybe<ContentParentId> mContentParentId;
+ // Strings share buffers if possible, so it's not a problem to duplicate the
+ // origin here.
+ const nsCString mOrigin;
+ uint64_t mDatastoreId;
+ bool mForPreload;
+ bool mInvalidated;
+
+ public:
+ PreparedDatastore(Datastore* aDatastore,
+ const Maybe<ContentParentId>& aContentParentId,
+ const nsACString& aOrigin, uint64_t aDatastoreId,
+ bool aForPreload)
+ : mDatastore(aDatastore),
+ mTimer(NS_NewTimer()),
+ mContentParentId(aContentParentId),
+ mOrigin(aOrigin),
+ mDatastoreId(aDatastoreId),
+ mForPreload(aForPreload),
+ mInvalidated(false) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatastore);
+ MOZ_ASSERT(mTimer);
+
+ aDatastore->NoteLivePreparedDatastore(this);
+
+ MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
+ TimerCallback, this, kPreparedDatastoreTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT, "PreparedDatastore::TimerCallback"));
+ }
+
+ ~PreparedDatastore() {
+ MOZ_ASSERT(mDatastore);
+ MOZ_ASSERT(mTimer);
+
+ mTimer->Cancel();
+
+ mDatastore->NoteFinishedPreparedDatastore(this);
+ }
+
+ const Datastore& DatastoreRef() const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDatastore);
+
+ return *mDatastore;
+ }
+
+ Datastore& MutableDatastoreRef() const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDatastore);
+
+ return *mDatastore;
+ }
+
+ const Maybe<ContentParentId>& GetContentParentId() const {
+ return mContentParentId;
+ }
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ void Invalidate() {
+ AssertIsOnBackgroundThread();
+
+ mInvalidated = true;
+
+ if (mForPreload) {
+ mTimer->Cancel();
+
+ MOZ_ALWAYS_SUCCEEDS(mTimer->InitWithNamedFuncCallback(
+ TimerCallback, this, 0, nsITimer::TYPE_ONE_SHOT,
+ "PreparedDatastore::TimerCallback"));
+ }
+ }
+
+ bool IsInvalidated() const {
+ AssertIsOnBackgroundThread();
+
+ return mInvalidated;
+ }
+
+ private:
+ void Destroy();
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+};
+
+/*******************************************************************************
+ * Actor class declarations
+ ******************************************************************************/
+
+class Database final
+ : public PBackgroundLSDatabaseParent,
+ public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ RefPtr<Datastore> mDatastore;
+ Snapshot* mSnapshot;
+ const PrincipalInfo mPrincipalInfo;
+ const Maybe<ContentParentId> mContentParentId;
+ // Strings share buffers if possible, so it's not a problem to duplicate the
+ // origin here.
+ nsCString mOrigin;
+ uint32_t mPrivateBrowsingId;
+ bool mAllowedToClose;
+ bool mActorDestroyed;
+ bool mRequestedAllowToClose;
+#ifdef DEBUG
+ bool mActorWasAlive;
+#endif
+
+ public:
+ // Created in AllocPBackgroundLSDatabaseParent.
+ Database(const PrincipalInfo& aPrincipalInfo,
+ const Maybe<ContentParentId>& aContentParentId,
+ const nsACString& aOrigin, uint32_t aPrivateBrowsingId);
+
+ Datastore* GetDatastore() const {
+ AssertIsOnBackgroundThread();
+ return mDatastore;
+ }
+
+ Maybe<Datastore&> MaybeDatastoreRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDatastore.get());
+ }
+
+ const PrincipalInfo& GetPrincipalInfo() const { return mPrincipalInfo; }
+
+ bool IsOwnedByProcess(ContentParentId aContentParentId) const {
+ return mContentParentId && mContentParentId.value() == aContentParentId;
+ }
+
+ uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; }
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ void SetActorAlive(Datastore* aDatastore);
+
+ void RegisterSnapshot(Snapshot* aSnapshot);
+
+ void UnregisterSnapshot(Snapshot* aSnapshot);
+
+ Snapshot* GetSnapshot() const {
+ AssertIsOnBackgroundThread();
+ return mSnapshot;
+ }
+
+ void RequestAllowToClose();
+
+ void ForceKill();
+
+ void Stringify(nsACString& aResult) const;
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Database, override)
+
+ private:
+ // Reference counted.
+ ~Database();
+
+ void AllowToClose();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+
+ mozilla::ipc::IPCResult RecvAllowToClose() override;
+
+ PBackgroundLSSnapshotParent* AllocPBackgroundLSSnapshotParent(
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) override;
+
+ mozilla::ipc::IPCResult RecvPBackgroundLSSnapshotConstructor(
+ PBackgroundLSSnapshotParent* aActor, const nsAString& aDocumentURI,
+ const nsAString& aKey, const bool& aIncreasePeakUsage,
+ const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) override;
+
+ bool DeallocPBackgroundLSSnapshotParent(
+ PBackgroundLSSnapshotParent* aActor) override;
+};
+
+/**
+ * Attempts to capture the state of the underlying Datastore at the time of its
+ * creation so run-to-completion semantics can be honored.
+ *
+ * Rather than simply duplicate the contents of `DataStore::mValues` and
+ * `Datastore::mOrderedItems` at the time of their creation, the Snapshot tracks
+ * mutations to the Datastore as they happen, saving off the state of values as
+ * they existed when the Snapshot was created. In other words, given an initial
+ * Datastore state of { foo: 'bar', bar: 'baz' }, the Snapshot won't store those
+ * values until it hears via `SaveItem` that "foo" is being over-written. At
+ * that time, it will save off foo='bar' in mValues.
+ *
+ * ## Quota Allocation ##
+ *
+ * ## States ##
+ *
+ */
+class Snapshot final : public PBackgroundLSSnapshotParent {
+ /**
+ * The Database that owns this snapshot. There is a 1:1 relationship between
+ * snapshots and databases.
+ */
+ RefPtr<Database> mDatabase;
+ RefPtr<Datastore> mDatastore;
+ /**
+ * The set of keys for which values have been sent to the child LSSnapshot.
+ * Cleared once all values have been sent as indicated by
+ * mLoadedItems.Count()==mTotalLength and therefore mLoadedAllItems should be
+ * true. No requests should be received for keys already in this set, and
+ * this is enforced by fatal IPC error (unless fuzzing).
+ */
+ nsTHashtable<nsStringHashKey> mLoadedItems;
+ /**
+ * The set of keys for which a RecvLoadValueAndMoreItems request was received
+ * but there was no such key, and so null was returned. The child LSSnapshot
+ * will also cache these values, so redundant requests are also handled with
+ * fatal process termination just like for mLoadedItems. Also cleared when
+ * mLoadedAllItems becomes true because then the child can infer that all
+ * other values must be null. (Note: this could also be done when
+ * mLoadKeysReceived is true as a further optimization, but is not.)
+ */
+ nsTHashSet<nsString> mUnknownItems;
+ /**
+ * Values that have changed in mDatastore as reported by SaveItem
+ * notifications that are not yet known to the child LSSnapshot.
+ *
+ * The naive way to snapshot the state of mDatastore would be to duplicate its
+ * internal mValues at the time of our creation, but that is wasteful if few
+ * changes are made to the Datastore's state. So we only track values that
+ * are changed/evicted from the Datastore as they happen, as reported to us by
+ * SaveItem notifications.
+ */
+ nsTHashMap<nsStringHashKey, LSValue> mValues;
+ /**
+ * Latched state of mDatastore's keys during a SaveItem notification with
+ * aAffectsOrder=true. The ordered keys needed to be saved off so that a
+ * consistent ordering could be presented to the child LSSnapshot when it asks
+ * for them via RecvLoadKeys.
+ */
+ nsTArray<nsString> mKeys;
+ nsString mDocumentURI;
+ /**
+ * The index used for restoring iteration over not yet sent key/value pairs to
+ * the child LSSnapshot.
+ */
+ uint32_t mNextLoadIndex;
+ /**
+ * The number of key/value pairs that were present in the Datastore at the
+ * time the snapshot was created. Once we have sent this many values to the
+ * child LSSnapshot, we can infer that it has received all of the keys/values
+ * and set mLoadedAllItems to true and clear mLoadedItems and mUnknownItems.
+ * Note that knowing the keys/values is not the same as knowing their ordering
+ * and so mKeys may be retained.
+ */
+ uint32_t mTotalLength;
+ int64_t mUsage;
+ int64_t mPeakUsage;
+ /**
+ * True if SaveItem has saved mDatastore's keys into mKeys because a SaveItem
+ * notification with aAffectsOrder=true was received.
+ */
+ bool mSavedKeys;
+ bool mActorDestroyed;
+ bool mFinishReceived;
+ bool mLoadedReceived;
+ /**
+ * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or
+ * LoadState::AllUnorderedItems. It will be AllOrderedItems if the initial
+ * snapshot contained all the data or if the state was AllOrderedKeys and
+ * successive RecvLoadValueAndMoreItems requests have resulted in the
+ * LSSnapshot being told all of the key/value pairs. It will be
+ * AllUnorderedItems if the state was LoadState::Partial and successive
+ * RecvLoadValueAndMoreItem requests got all the keys/values but the key
+ * ordering was not retrieved.
+ */
+ bool mLoadedAllItems;
+ /**
+ * True if LSSnapshot's mLoadState should be LoadState::AllOrderedItems or
+ * AllOrderedKeys. This can occur because of the initial snapshot, or because
+ * a RecvLoadKeys request was received.
+ */
+ bool mLoadKeysReceived;
+ bool mSentMarkDirty;
+
+ /**
+ * True if there are Database objects in other content processes. The value
+ * never gets updated, we instead mark snapshots as dirty when Database
+ * objects are added or removed. Marking snapshots as dirty forces creation
+ * of new snapshots for new tasks.
+ */
+ bool mHasOtherProcessDatabases;
+ bool mHasOtherProcessObservers;
+
+ public:
+ // Created in AllocPBackgroundLSSnapshotParent.
+ Snapshot(Database* aDatabase, const nsAString& aDocumentURI);
+
+ void Init(nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTHashSet<nsString>&& aUnknownItems, uint32_t aNextLoadIndex,
+ uint32_t aTotalLength, int64_t aUsage, int64_t aPeakUsage,
+ LSSnapshot::LoadState aLoadState, bool aHasOtherProcessDatabases,
+ bool aHasOtherProcessObservers) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aUsage >= 0);
+ MOZ_ASSERT(aPeakUsage >= aUsage);
+ MOZ_ASSERT_IF(aLoadState != LSSnapshot::LoadState::AllOrderedItems,
+ aNextLoadIndex < aTotalLength);
+ MOZ_ASSERT(mTotalLength == 0);
+ MOZ_ASSERT(mUsage == -1);
+ MOZ_ASSERT(mPeakUsage == -1);
+
+ mLoadedItems.SwapElements(aLoadedItems);
+ mUnknownItems = std::move(aUnknownItems);
+ mNextLoadIndex = aNextLoadIndex;
+ mTotalLength = aTotalLength;
+ mUsage = aUsage;
+ mPeakUsage = aPeakUsage;
+ if (aLoadState == LSSnapshot::LoadState::AllOrderedKeys) {
+ MOZ_ASSERT(mUnknownItems.Count() == 0);
+ mLoadKeysReceived = true;
+ } else if (aLoadState == LSSnapshot::LoadState::AllOrderedItems) {
+ MOZ_ASSERT(mLoadedItems.Count() == 0);
+ MOZ_ASSERT(mUnknownItems.Count() == 0);
+ MOZ_ASSERT(mNextLoadIndex == mTotalLength);
+ mLoadedReceived = true;
+ mLoadedAllItems = true;
+ mLoadKeysReceived = true;
+ }
+ mHasOtherProcessDatabases = aHasOtherProcessDatabases;
+ mHasOtherProcessObservers = aHasOtherProcessObservers;
+ }
+
+ /**
+ * Called via NotifySnapshots by Datastore whenever it is updating its
+ * internal state so that snapshots can save off the state of a value at the
+ * time of their creation.
+ */
+ void SaveItem(const nsAString& aKey, const LSValue& aOldValue,
+ bool aAffectsOrder);
+
+ void MarkDirty();
+
+ bool IsDirty() const {
+ AssertIsOnBackgroundThread();
+
+ return mSentMarkDirty;
+ }
+
+ bool HasOtherProcessDatabases() const {
+ AssertIsOnBackgroundThread();
+
+ return mHasOtherProcessDatabases;
+ }
+
+ bool HasOtherProcessObservers() const {
+ AssertIsOnBackgroundThread();
+
+ return mHasOtherProcessObservers;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot)
+
+ private:
+ // Reference counted.
+ ~Snapshot();
+
+ mozilla::ipc::IPCResult Checkpoint(nsTArray<LSWriteInfo>&& aWriteInfos);
+
+ mozilla::ipc::IPCResult CheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos);
+
+ void Finish();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+
+ mozilla::ipc::IPCResult RecvAsyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+ mozilla::ipc::IPCResult RecvAsyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override;
+
+ mozilla::ipc::IPCResult RecvSyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) override;
+
+ mozilla::ipc::IPCResult RecvSyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override;
+
+ mozilla::ipc::IPCResult RecvAsyncFinish() override;
+
+ mozilla::ipc::IPCResult RecvSyncFinish() override;
+
+ mozilla::ipc::IPCResult RecvLoaded() override;
+
+ mozilla::ipc::IPCResult RecvLoadValueAndMoreItems(
+ const nsAString& aKey, LSValue* aValue,
+ nsTArray<LSItemInfo>* aItemInfos) override;
+
+ mozilla::ipc::IPCResult RecvLoadKeys(nsTArray<nsString>* aKeys) override;
+
+ mozilla::ipc::IPCResult RecvIncreasePeakUsage(const int64_t& aMinSize,
+ int64_t* aSize) override;
+};
+
+class Observer final : public PBackgroundLSObserverParent {
+ nsCString mOrigin;
+ bool mActorDestroyed;
+
+ public:
+ // Created in AllocPBackgroundLSObserverParent.
+ explicit Observer(const nsACString& aOrigin);
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ void Observe(Database* aDatabase, const nsString& aDocumentURI,
+ const nsString& aKey, const LSValue& aOldValue,
+ const LSValue& aNewValue);
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Observer)
+
+ private:
+ // Reference counted.
+ ~Observer();
+
+ // IPDL methods are only called by IPDL.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvDeleteMe() override;
+};
+
+class LSRequestBase : public DatastoreOperationBase,
+ public PBackgroundLSRequestParent {
+ protected:
+ enum class State {
+ // Just created on the PBackground thread. Next step is StartingRequest.
+ Initial,
+
+ // Waiting to start/starting request on the PBackground thread. Next step is
+ // either Nesting if a subclass needs to process more nested states or
+ // SendingReadyMessage if a subclass doesn't need any nested processing.
+ StartingRequest,
+
+ // Doing nested processing.
+ Nesting,
+
+ // Waiting to send/sending the ready message on the PBackground thread. Next
+ // step is WaitingForFinish.
+ SendingReadyMessage,
+
+ // Waiting for the finish message on the PBackground thread. Next step is
+ // SendingResults.
+ WaitingForFinish,
+
+ // Waiting to send/sending results on the PBackground thread. Next step is
+ // Completed.
+ SendingResults,
+
+ // All done.
+ Completed
+ };
+
+ const LSRequestParams mParams;
+ Maybe<ContentParentId> mContentParentId;
+ State mState;
+ bool mWaitingForFinish;
+
+ public:
+ LSRequestBase(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ void Dispatch();
+
+ void StringifyState(nsACString& aResult) const;
+
+ virtual void Stringify(nsACString& aResult) const;
+
+ virtual void Log();
+
+ protected:
+ ~LSRequestBase() override;
+
+ virtual nsresult Start() = 0;
+
+ virtual nsresult NestedRun();
+
+ virtual void GetResponse(LSRequestResponse& aResponse) = 0;
+
+ virtual void Cleanup() {}
+
+ private:
+ bool VerifyRequestParams();
+
+ nsresult StartRequest();
+
+ void SendReadyMessage();
+
+ nsresult SendReadyMessageInternal();
+
+ void Finish();
+
+ void FinishInternal();
+
+ void SendResults();
+
+ protected:
+ // Common nsIRunnable implementation that subclasses may not override.
+ NS_IMETHOD
+ Run() final;
+
+ // IPDL methods.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ mozilla::ipc::IPCResult RecvCancel() final;
+
+ mozilla::ipc::IPCResult RecvFinish() final;
+};
+
+class PrepareDatastoreOp
+ : public LSRequestBase,
+ public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ class LoadDataOp;
+
+ class CompressFunction;
+ class CompressionTypeFunction;
+
+ enum class NestedState {
+ // The nesting has not yet taken place. Next step is
+ // CheckExistingOperations.
+ BeforeNesting,
+
+ // Checking if a prepare datastore operation is already running for given
+ // origin on the PBackground thread. Next step is CheckClosingDatastore.
+ CheckExistingOperations,
+
+ // Checking if a datastore is closing the connection for given origin on
+ // the PBackground thread. Next step is PreparationPending.
+ CheckClosingDatastore,
+
+ // 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.
+ // If a datastore already exists for given origin then the next state is
+ // SendingReadyMessage.
+ PreparationPending,
+
+ // Waiting for directory open allowed on the PBackground thread. The next
+ // step is either SendingReadyMessage if directory lock failed to acquire,
+ // or DatabaseWorkOpen if directory lock is acquired.
+ DirectoryOpenPending,
+
+ // Waiting to do/doing work on the QuotaManager IO thread. Its next step is
+ // BeginLoadData.
+ DatabaseWorkOpen,
+
+ // Starting a load data operation on the PBackground thread. Next step is
+ // DatabaseWorkLoadData.
+ BeginLoadData,
+
+ // Waiting to do/doing work on the connection thread. This involves waiting
+ // for the LoadDataOp to do its work. Eventually the state will transition
+ // to SendingReadyMessage.
+ DatabaseWorkLoadData,
+
+ // The nesting has completed.
+ AfterNesting
+ };
+
+ RefPtr<PrepareDatastoreOp> mDelayedOp;
+ RefPtr<ClientDirectoryLock> mPendingDirectoryLock;
+ RefPtr<DirectoryLock> mDirectoryLock;
+ RefPtr<Connection> mConnection;
+ RefPtr<Datastore> mDatastore;
+ UniquePtr<ArchivedOriginScope> mArchivedOriginScope;
+ LoadDataOp* mLoadDataOp;
+ nsTHashMap<nsStringHashKey, LSValue> mValues;
+ nsTArray<LSItemInfo> mOrderedItems;
+ OriginMetadata mOriginMetadata;
+ nsCString mMainThreadOrigin;
+ nsString mDatabaseFilePath;
+ uint32_t mPrivateBrowsingId;
+ int64_t mUsage;
+ int64_t mSizeOfKeys;
+ int64_t mSizeOfItems;
+ uint64_t mDatastoreId;
+ NestedState mNestedState;
+ const bool mForPreload;
+ bool mDatabaseNotAvailable;
+ // Set when the Datastore has been registered with gPrivateDatastores so that
+ // it can be unregistered if an error is encountered in PrepareDatastoreOp.
+ FlippedOnce<false> mPrivateDatastoreRegistered;
+ // Set when the Datastore has been registered with gPreparedDatastores so
+ // that it can be unregistered if an error is encountered in
+ // PrepareDatastoreOp.
+ FlippedOnce<false> mPreparedDatastoreRegistered;
+ bool mInvalidated;
+
+#ifdef DEBUG
+ int64_t mDEBUGUsage;
+#endif
+
+ public:
+ PrepareDatastoreOp(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ Maybe<DirectoryLock&> MaybeDirectoryLockRef() const {
+ AssertIsOnBackgroundThread();
+
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ bool OriginIsKnown() const {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+
+ return !mOriginMetadata.mOrigin.IsEmpty();
+ }
+
+ const nsCString& Origin() const {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+ MOZ_ASSERT(OriginIsKnown());
+
+ return mOriginMetadata.mOrigin;
+ }
+
+ void Invalidate() {
+ AssertIsOnOwningThread();
+
+ mInvalidated = true;
+ }
+
+ void StringifyNestedState(nsACString& aResult) const;
+
+ void Stringify(nsACString& aResult) const override;
+
+ void Log() override;
+
+ private:
+ ~PrepareDatastoreOp() override;
+
+ nsresult Start() override;
+
+ nsresult CheckExistingOperations();
+
+ nsresult CheckClosingDatastoreInternal();
+
+ nsresult CheckClosingDatastore();
+
+ nsresult BeginDatastorePreparationInternal();
+
+ nsresult BeginDatastorePreparation();
+
+ void SendToIOThread();
+
+ nsresult DatabaseWork();
+
+ nsresult DatabaseNotAvailable();
+
+ nsresult EnsureDirectoryEntry(nsIFile* aEntry, bool aCreateIfNotExists,
+ bool aDirectory,
+ bool* aAlreadyExisted = nullptr);
+
+ nsresult VerifyDatabaseInformation(mozIStorageConnection* aConnection);
+
+ already_AddRefed<QuotaObject> GetQuotaObject();
+
+ nsresult BeginLoadData();
+
+ void FinishNesting();
+
+ nsresult FinishNestingOnNonOwningThread();
+
+ nsresult NestedRun() override;
+
+ void GetResponse(LSRequestResponse& aResponse) override;
+
+ void Cleanup() override;
+
+ void ConnectionClosedCallback();
+
+ void CleanupMetadata();
+
+ // IPDL overrides.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ void DirectoryLockAcquired(DirectoryLock* aLock);
+
+ void DirectoryLockFailed();
+};
+
+class PrepareDatastoreOp::LoadDataOp final
+ : public ConnectionDatastoreOperationBase {
+ RefPtr<PrepareDatastoreOp> mPrepareDatastoreOp;
+
+ public:
+ explicit LoadDataOp(PrepareDatastoreOp* aPrepareDatastoreOp)
+ : ConnectionDatastoreOperationBase(aPrepareDatastoreOp->mConnection),
+ mPrepareDatastoreOp(aPrepareDatastoreOp) {}
+
+ private:
+ ~LoadDataOp() = default;
+
+ nsresult DoDatastoreWork() override;
+
+ void OnSuccess() override;
+
+ void OnFailure(nsresult aResultCode) override;
+
+ void Cleanup() override;
+};
+
+class PrepareDatastoreOp::CompressFunction final : public mozIStorageFunction {
+ private:
+ ~CompressFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+class PrepareDatastoreOp::CompressionTypeFunction final
+ : public mozIStorageFunction {
+ private:
+ ~CompressionTypeFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+class PrepareObserverOp : public LSRequestBase {
+ nsCString mOrigin;
+
+ public:
+ PrepareObserverOp(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ private:
+ nsresult Start() override;
+
+ void GetResponse(LSRequestResponse& aResponse) override;
+};
+
+class LSSimpleRequestBase : public DatastoreOperationBase,
+ public PBackgroundLSSimpleRequestParent {
+ protected:
+ enum class State {
+ // Just created on the PBackground thread. Next step is StartingRequest.
+ Initial,
+
+ // Waiting to start/starting request on the PBackground thread. Next step is
+ // SendingResults.
+ StartingRequest,
+
+ // Waiting to send/sending results on the PBackground thread. Next step is
+ // Completed.
+ SendingResults,
+
+ // All done.
+ Completed
+ };
+
+ const LSSimpleRequestParams mParams;
+ Maybe<ContentParentId> mContentParentId;
+ State mState;
+
+ public:
+ LSSimpleRequestBase(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ void Dispatch();
+
+ protected:
+ ~LSSimpleRequestBase() override;
+
+ virtual nsresult Start() = 0;
+
+ virtual void GetResponse(LSSimpleRequestResponse& aResponse) = 0;
+
+ private:
+ bool VerifyRequestParams();
+
+ nsresult StartRequest();
+
+ void SendResults();
+
+ // Common nsIRunnable implementation that subclasses may not override.
+ NS_IMETHOD
+ Run() final;
+
+ // IPDL methods.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+};
+
+class PreloadedOp : public LSSimpleRequestBase {
+ nsCString mOrigin;
+
+ public:
+ PreloadedOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ private:
+ nsresult Start() override;
+
+ void GetResponse(LSSimpleRequestResponse& aResponse) override;
+};
+
+class GetStateOp : public LSSimpleRequestBase {
+ nsCString mOrigin;
+
+ public:
+ GetStateOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId);
+
+ private:
+ nsresult Start() override;
+
+ void GetResponse(LSSimpleRequestResponse& aResponse) override;
+};
+
+/*******************************************************************************
+ * Other class declarations
+ ******************************************************************************/
+
+struct ArchivedOriginInfo {
+ OriginAttributes mOriginAttributes;
+ nsCString mOriginNoSuffix;
+
+ ArchivedOriginInfo(const OriginAttributes& aOriginAttributes,
+ const nsACString& aOriginNoSuffix)
+ : mOriginAttributes(aOriginAttributes),
+ mOriginNoSuffix(aOriginNoSuffix) {}
+};
+
+class ArchivedOriginScope {
+ struct Origin {
+ nsCString mOriginSuffix;
+ nsCString mOriginNoSuffix;
+
+ Origin(const nsACString& aOriginSuffix, const nsACString& aOriginNoSuffix)
+ : mOriginSuffix(aOriginSuffix), mOriginNoSuffix(aOriginNoSuffix) {}
+
+ const nsACString& OriginSuffix() const { return mOriginSuffix; }
+
+ const nsACString& OriginNoSuffix() const { return mOriginNoSuffix; }
+ };
+
+ struct Prefix {
+ nsCString mOriginNoSuffix;
+
+ explicit Prefix(const nsACString& aOriginNoSuffix)
+ : mOriginNoSuffix(aOriginNoSuffix) {}
+
+ const nsACString& OriginNoSuffix() const { return mOriginNoSuffix; }
+ };
+
+ struct Pattern {
+ UniquePtr<OriginAttributesPattern> mPattern;
+
+ explicit Pattern(const OriginAttributesPattern& aPattern)
+ : mPattern(MakeUnique<OriginAttributesPattern>(aPattern)) {}
+
+ Pattern(const Pattern& aOther)
+ : mPattern(MakeUnique<OriginAttributesPattern>(*aOther.mPattern)) {}
+
+ Pattern(Pattern&& aOther) = default;
+
+ const OriginAttributesPattern& GetPattern() const {
+ MOZ_ASSERT(mPattern);
+ return *mPattern;
+ }
+ };
+
+ struct Null {};
+
+ using DataType = Variant<Origin, Pattern, Prefix, Null>;
+
+ DataType mData;
+
+ public:
+ static UniquePtr<ArchivedOriginScope> CreateFromOrigin(
+ const nsACString& aOriginAttrSuffix, const nsACString& aOriginKey);
+
+ static UniquePtr<ArchivedOriginScope> CreateFromPrefix(
+ const nsACString& aOriginKey);
+
+ static UniquePtr<ArchivedOriginScope> CreateFromPattern(
+ const OriginAttributesPattern& aPattern);
+
+ static UniquePtr<ArchivedOriginScope> CreateFromNull();
+
+ bool IsOrigin() const { return mData.is<Origin>(); }
+
+ bool IsPrefix() const { return mData.is<Prefix>(); }
+
+ bool IsPattern() const { return mData.is<Pattern>(); }
+
+ bool IsNull() const { return mData.is<Null>(); }
+
+ const nsACString& OriginSuffix() const {
+ MOZ_ASSERT(IsOrigin());
+
+ return mData.as<Origin>().OriginSuffix();
+ }
+
+ const nsACString& OriginNoSuffix() const {
+ MOZ_ASSERT(IsOrigin() || IsPrefix());
+
+ if (IsOrigin()) {
+ return mData.as<Origin>().OriginNoSuffix();
+ }
+ return mData.as<Prefix>().OriginNoSuffix();
+ }
+
+ const OriginAttributesPattern& GetPattern() const {
+ MOZ_ASSERT(IsPattern());
+
+ return mData.as<Pattern>().GetPattern();
+ }
+
+ nsLiteralCString GetBindingClause() const;
+
+ nsresult BindToStatement(mozIStorageStatement* aStatement) const;
+
+ bool HasMatches(ArchivedOriginHashtable* aHashtable) const;
+
+ void RemoveMatches(ArchivedOriginHashtable* aHashtable) const;
+
+ private:
+ // Move constructors
+ explicit ArchivedOriginScope(const Origin&& aOrigin) : mData(aOrigin) {}
+
+ explicit ArchivedOriginScope(const Pattern&& aPattern) : mData(aPattern) {}
+
+ explicit ArchivedOriginScope(const Prefix&& aPrefix) : mData(aPrefix) {}
+
+ explicit ArchivedOriginScope(const Null&& aNull) : mData(aNull) {}
+};
+
+class QuotaClient final : public mozilla::dom::quota::Client {
+ class MatchFunction;
+
+ static QuotaClient* sInstance;
+
+ Mutex mShadowDatabaseMutex MOZ_UNANNOTATED;
+
+ public:
+ QuotaClient();
+
+ static QuotaClient* GetInstance() {
+ AssertIsOnBackgroundThread();
+
+ return sInstance;
+ }
+
+ mozilla::Mutex& ShadowDatabaseMutex() {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+
+ return mShadowDatabaseMutex;
+ }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override)
+
+ Type GetType() 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;
+
+ nsresult AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope) 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;
+
+ Result<UniquePtr<ArchivedOriginScope>, nsresult> CreateArchivedOriginScope(
+ const OriginScope& aOriginScope);
+
+ nsresult PerformDelete(mozIStorageConnection* aConnection,
+ const nsACString& aSchemaName,
+ ArchivedOriginScope* aArchivedOriginScope) const;
+};
+
+class QuotaClient::MatchFunction final : public mozIStorageFunction {
+ OriginAttributesPattern mPattern;
+
+ public:
+ explicit MatchFunction(const OriginAttributesPattern& aPattern)
+ : mPattern(aPattern) {}
+
+ private:
+ ~MatchFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+/*******************************************************************************
+ * Helper classes
+ ******************************************************************************/
+
+class MOZ_STACK_CLASS AutoWriteTransaction final {
+ Connection* mConnection;
+ Maybe<MutexAutoLock> mShadowDatabaseLock;
+ bool mShadowWrites;
+
+ public:
+ explicit AutoWriteTransaction(bool aShadowWrites);
+
+ ~AutoWriteTransaction();
+
+ nsresult Start(Connection* aConnection);
+
+ nsresult Commit();
+
+ private:
+ nsresult LockAndAttachShadowDatabase(Connection* aConnection);
+
+ nsresult DetachShadowDatabaseAndUnlock();
+};
+
+/*******************************************************************************
+ * Globals
+ ******************************************************************************/
+
+#ifdef DEBUG
+bool gLocalStorageInitialized = false;
+#endif
+
+using PrepareDatastoreOpArray =
+ nsTArray<NotNull<CheckedUnsafePtr<PrepareDatastoreOp>>>;
+
+StaticAutoPtr<PrepareDatastoreOpArray> gPrepareDatastoreOps;
+
+// nsCStringHashKey with disabled memmove
+class nsCStringHashKeyDM : public nsCStringHashKey {
+ public:
+ explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey)
+ : nsCStringHashKey(aKey) {}
+ enum { ALLOW_MEMMOVE = false };
+};
+
+// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that
+// the hashtable uses the copy constructor instead of memmove for moving entries
+// since memmove will break CheckedUnsafePtr in a memory-corrupting way.
+using DatastoreHashKey =
+ std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM,
+ nsCStringHashKey>::type;
+
+using DatastoreHashtable =
+ nsBaseHashtable<DatastoreHashKey, NotNull<CheckedUnsafePtr<Datastore>>,
+ MovingNotNull<CheckedUnsafePtr<Datastore>>>;
+
+StaticAutoPtr<DatastoreHashtable> gDatastores;
+
+uint64_t gLastDatastoreId = 0;
+
+using PreparedDatastoreHashtable =
+ nsClassHashtable<nsUint64HashKey, PreparedDatastore>;
+
+StaticAutoPtr<PreparedDatastoreHashtable> gPreparedDatastores;
+
+using PrivateDatastoreHashtable =
+ nsClassHashtable<nsCStringHashKey, PrivateDatastore>;
+
+// Keeps Private Browsing Datastores alive until the private browsing session
+// is closed. This is necessary because LocalStorage Private Browsing data is
+// (currently) not written to disk and therefore needs to explicitly be kept
+// alive in memory so that if a user browses away from a site during a session
+// and then back to it that they will still have their data.
+//
+// The entries are wrapped by PrivateDatastore instances which call
+// NoteLivePrivateDatastore and NoteFinishedPrivateDatastore which set and
+// clear mHasLivePrivateDatastore which inhibits MaybeClose() from closing the
+// datastore (which would discard the data) when there are no active windows
+// using LocalStorage for the origin.
+//
+// The table is cleared when the Private Browsing session is closed, which will
+// cause NoteFinishedPrivateDatastore to be called on each Datastore which will
+// in turn call MaybeClose which should then discard the Datastore. Or in the
+// event of an (unlikely) race where the private browsing windows are still
+// being torn down, will cause the Datastore to be discarded when the last
+// window actually goes away.
+UniquePtr<PrivateDatastoreHashtable> gPrivateDatastores;
+
+using LiveDatabaseArray = nsTArray<NotNull<CheckedUnsafePtr<Database>>>;
+
+StaticAutoPtr<LiveDatabaseArray> gLiveDatabases;
+
+StaticRefPtr<ConnectionThread> gConnectionThread;
+
+uint64_t gLastObserverId = 0;
+
+using PreparedObserverHashtable = nsRefPtrHashtable<nsUint64HashKey, Observer>;
+
+StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers;
+
+using ObserverHashtable =
+ nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<Observer*>>>;
+
+StaticAutoPtr<ObserverHashtable> gObservers;
+
+Atomic<bool> gShadowWrites(kDefaultShadowWrites);
+Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill);
+Atomic<int32_t, Relaxed> gSnapshotGradualPrefill(
+ kDefaultSnapshotGradualPrefill);
+Atomic<bool> gClientValidation(kDefaultClientValidation);
+
+using UsageHashtable = nsTHashMap<nsCStringHashKey, int64_t>;
+
+StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins;
+
+// Can only be touched on the Quota Manager I/O thread.
+bool gInitializedShadowStorage = false;
+
+StaticAutoPtr<LSInitializationInfo> gInitializationInfo;
+
+bool IsOnGlobalConnectionThread() {
+ MOZ_ASSERT(gConnectionThread);
+ return gConnectionThread->IsOnConnectionThread();
+}
+
+void AssertIsOnGlobalConnectionThread() {
+ MOZ_ASSERT(gConnectionThread);
+ gConnectionThread->AssertIsOnConnectionThread();
+}
+
+already_AddRefed<Datastore> GetDatastore(const nsACString& aOrigin) {
+ AssertIsOnBackgroundThread();
+
+ if (gDatastores) {
+ auto maybeDatastore = gDatastores->MaybeGet(aOrigin);
+ if (maybeDatastore) {
+ RefPtr<Datastore> result(std::move(*maybeDatastore).unwrapBasePtr());
+ return result.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult LoadArchivedOrigins() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!gArchivedOrigins);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_INSPECT(const auto& connection, CreateArchiveStorageConnection(
+ quotaManager->GetStoragePath()));
+
+ if (!connection) {
+ gArchivedOrigins = new ArchivedOriginHashtable();
+ return NS_OK;
+ }
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "SELECT DISTINCT originAttributes, originKey "
+ "FROM webappsstore2;"_ns));
+
+ auto archivedOrigins = MakeUnique<ArchivedOriginHashtable>();
+
+ // XXX Actually, this could use a hashtable variant of
+ // CollectElementsWhileHasResult
+ QM_TRY(quota::CollectWhileHasResult(
+ *stmt, [&archivedOrigins](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& originSuffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 0));
+ QM_TRY_INSPECT(const auto& originNoSuffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 1));
+
+ const nsCString hashKey =
+ GetArchivedOriginHashKey(originSuffix, originNoSuffix);
+
+ OriginAttributes originAttributes;
+ QM_TRY(OkIf(originAttributes.PopulateFromSuffix(originSuffix)),
+ Err(NS_ERROR_FAILURE));
+
+ archivedOrigins->InsertOrUpdate(
+ hashKey,
+ MakeUnique<ArchivedOriginInfo>(originAttributes, originNoSuffix));
+
+ return Ok{};
+ }));
+
+ gArchivedOrigins = archivedOrigins.release();
+ return NS_OK;
+}
+
+Result<int64_t, nsresult> GetUsage(mozIStorageConnection& aConnection,
+ ArchivedOriginScope* aArchivedOriginScope) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ ([aArchivedOriginScope,
+ &aConnection]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> {
+ if (aArchivedOriginScope) {
+ QM_TRY_RETURN(CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection,
+ "SELECT "
+ "total(utf16Length(key) + utf16Length(value)) "
+ "FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns,
+ [aArchivedOriginScope](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ aArchivedOriginScope->BindToStatement(&stmt)));
+ return Ok{};
+ }));
+ }
+
+ QM_TRY_RETURN(CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection, "SELECT usage FROM database"_ns));
+ }()));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0));
+}
+
+void ShadowWritesPrefChangedCallback(const char* aPrefName, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kShadowWritesPref));
+ MOZ_ASSERT(!aClosure);
+
+ gShadowWrites = Preferences::GetBool(aPrefName, kDefaultShadowWrites);
+}
+
+void SnapshotPrefillPrefChangedCallback(const char* aPrefName, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kSnapshotPrefillPref));
+ MOZ_ASSERT(!aClosure);
+
+ int32_t snapshotPrefill =
+ Preferences::GetInt(aPrefName, kDefaultSnapshotPrefill);
+
+ // The magic -1 is for use only by tests.
+ if (snapshotPrefill == -1) {
+ snapshotPrefill = INT32_MAX;
+ }
+
+ gSnapshotPrefill = snapshotPrefill;
+}
+
+void SnapshotGradualPrefillPrefChangedCallback(const char* aPrefName,
+ void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kSnapshotGradualPrefillPref));
+ MOZ_ASSERT(!aClosure);
+
+ int32_t snapshotGradualPrefill =
+ Preferences::GetInt(aPrefName, kDefaultSnapshotGradualPrefill);
+
+ // The magic -1 is for use only by tests.
+ if (snapshotGradualPrefill == -1) {
+ snapshotGradualPrefill = INT32_MAX;
+ }
+
+ gSnapshotGradualPrefill = snapshotGradualPrefill;
+}
+
+int64_t GetSnapshotPeakUsagePreincrement(bool aInitial) {
+ return aInitial ? StaticPrefs::
+ dom_storage_snapshot_peak_usage_initial_preincrement()
+ : StaticPrefs::
+ dom_storage_snapshot_peak_usage_gradual_preincrement();
+}
+
+int64_t GetSnapshotPeakUsageReducedPreincrement(bool aInitial) {
+ return aInitial
+ ? StaticPrefs::
+ dom_storage_snapshot_peak_usage_reduced_initial_preincrement()
+ : StaticPrefs::
+ dom_storage_snapshot_peak_usage_reduced_gradual_preincrement();
+}
+
+void ClientValidationPrefChangedCallback(const char* aPrefName,
+ void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kClientValidationPref));
+ MOZ_ASSERT(!aClosure);
+
+ gClientValidation = Preferences::GetBool(aPrefName, kDefaultClientValidation);
+}
+
+template <typename Condition>
+void InvalidatePrepareDatastoreOpsMatching(const Condition& aCondition) {
+ if (!gPrepareDatastoreOps) {
+ return;
+ }
+
+ for (const auto& prepareDatastoreOp : *gPrepareDatastoreOps) {
+ if (aCondition(*prepareDatastoreOp)) {
+ prepareDatastoreOp->Invalidate();
+ }
+ }
+}
+
+template <typename Condition>
+void InvalidatePreparedDatastoresMatching(const Condition& aCondition) {
+ if (!gPreparedDatastores) {
+ return;
+ }
+
+ for (const auto& preparedDatastore : gPreparedDatastores->Values()) {
+ MOZ_ASSERT(preparedDatastore);
+
+ if (aCondition(*preparedDatastore)) {
+ preparedDatastore->Invalidate();
+ }
+ }
+}
+
+template <typename Condition>
+nsTArray<RefPtr<Database>> CollectDatabasesMatching(Condition aCondition) {
+ AssertIsOnBackgroundThread();
+
+ if (!gLiveDatabases) {
+ return nsTArray<RefPtr<Database>>{};
+ }
+
+ nsTArray<RefPtr<Database>> databases;
+
+ for (const auto& database : *gLiveDatabases) {
+ if (aCondition(*database)) {
+ databases.AppendElement(database.get());
+ }
+ }
+
+ return databases;
+}
+
+template <typename Condition>
+void RequestAllowToCloseDatabasesMatching(Condition aCondition) {
+ AssertIsOnBackgroundThread();
+
+ nsTArray<RefPtr<Database>> databases = CollectDatabasesMatching(aCondition);
+
+ for (const auto& database : databases) {
+ MOZ_ASSERT(database);
+
+ database->RequestAllowToClose();
+ }
+}
+
+void ForceKillAllDatabases() {
+ AssertIsOnBackgroundThread();
+
+ nsTArray<RefPtr<Database>> databases =
+ CollectDatabasesMatching([](const auto&) { return true; });
+
+ for (const auto& database : databases) {
+ MOZ_ASSERT(database);
+
+ database->ForceKill();
+ }
+}
+
+bool VerifyPrincipalInfo(const PrincipalInfo& aPrincipalInfo,
+ const PrincipalInfo& aStoragePrincipalInfo,
+ bool aCheckClientPrincipal) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) {
+ return false;
+ }
+
+ // Note that the client prinicpal could have a different spec than the node
+ // principal but they should have the same origin. It's because the client
+ // could be initialized when opening the initial about:blank document and pass
+ // to the newly opened window and reuse over there if the new window has the
+ // same origin as the initial about:blank document. But, the FilePath could be
+ // different. Therefore, we have to ignore comparing the Spec of the
+ // principals if we are verifying clinet principal here. Also, when
+ // document.domain is set, client principal won't get it. So, we don't compare
+ // domain for client princpal too.
+ bool result = aCheckClientPrincipal
+ ? StoragePrincipalHelper::
+ VerifyValidClientPrincipalInfoForPrincipalInfo(
+ aStoragePrincipalInfo, aPrincipalInfo)
+ : StoragePrincipalHelper::
+ VerifyValidStoragePrincipalInfoForPrincipalInfo(
+ aStoragePrincipalInfo, aPrincipalInfo);
+ if (NS_WARN_IF(!result)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool VerifyClientId(const Maybe<ContentParentId>& aContentParentId,
+ const Maybe<PrincipalInfo>& aPrincipalInfo,
+ const Maybe<nsID>& aClientId) {
+ AssertIsOnBackgroundThread();
+
+ if (gClientValidation) {
+ if (NS_WARN_IF(aClientId.isNothing())) {
+ return false;
+ }
+
+ if (NS_WARN_IF(aPrincipalInfo.isNothing())) {
+ return false;
+ }
+
+ RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance();
+ if (svc && NS_WARN_IF(!svc->HasWindow(
+ aContentParentId, aPrincipalInfo.ref(), aClientId.ref()))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool VerifyOriginKey(const nsACString& aOriginKey,
+ const PrincipalInfo& aPrincipalInfo) {
+ AssertIsOnBackgroundThread();
+
+ QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(aPrincipalInfo), false);
+
+ Unused << originAttrSuffix;
+
+ QM_TRY(OkIf(originKey == aOriginKey), false,
+ ([&originKey = originKey, &aOriginKey](const auto) {
+ LS_WARNING("originKey (%s) doesn't match passed one (%s)!",
+ originKey.get(), nsCString(aOriginKey).get());
+ }));
+
+ return true;
+}
+
+LSInitializationInfo& MutableInitializationInfoRef(const CreateIfNonExistent&) {
+ if (!gInitializationInfo) {
+ gInitializationInfo = new LSInitializationInfo();
+ }
+ return *gInitializationInfo;
+}
+
+template <typename Func>
+auto ExecuteOriginInitialization(const nsACString& aOrigin,
+ const LSOriginInitialization aInitialization,
+ const nsACString& aContext, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ LSOriginInitialization, Nothing>&> {
+ return ExecuteInitialization(
+ MutableInitializationInfoRef(CreateIfNonExistent{})
+ .MutableOriginInitializationInfoRef(aOrigin, CreateIfNonExistent{}),
+ aInitialization, aContext, std::forward<Func>(aFunc));
+}
+
+} // namespace
+
+/*******************************************************************************
+ * Exported functions
+ ******************************************************************************/
+
+void InitializeLocalStorage() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gLocalStorageInitialized);
+
+ // XXX Isn't this redundant? It's already done in InitializeQuotaManager.
+ if (!QuotaManager::IsRunningGTests()) {
+ // This service has to be started on the main thread currently.
+ const nsCOMPtr<mozIStorageService> ss =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+
+ QM_WARNONLY_TRY(OkIf(ss));
+ }
+
+ Preferences::RegisterCallbackAndCall(ShadowWritesPrefChangedCallback,
+ kShadowWritesPref);
+
+ Preferences::RegisterCallbackAndCall(SnapshotPrefillPrefChangedCallback,
+ kSnapshotPrefillPref);
+
+ Preferences::RegisterCallbackAndCall(
+ SnapshotGradualPrefillPrefChangedCallback, kSnapshotGradualPrefillPref);
+
+ Preferences::RegisterCallbackAndCall(ClientValidationPrefChangedCallback,
+ kClientValidationPref);
+
+#ifdef DEBUG
+ gLocalStorageInitialized = true;
+#endif
+}
+
+already_AddRefed<PBackgroundLSDatabaseParent> AllocPBackgroundLSDatabaseParent(
+ const PrincipalInfo& aPrincipalInfo, const uint32_t& aPrivateBrowsingId,
+ const uint64_t& aDatastoreId) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!gPreparedDatastores)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId);
+ if (NS_WARN_IF(!preparedDatastore)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ // If we ever decide to return null from this point on, we need to make sure
+ // that the datastore is closed and the prepared datastore is removed from the
+ // gPreparedDatastores hashtable.
+ // We also assume that IPDL must call RecvPBackgroundLSDatabaseConstructor
+ // once we return a valid actor in this method.
+
+ RefPtr<Database> database =
+ new Database(aPrincipalInfo, preparedDatastore->GetContentParentId(),
+ preparedDatastore->Origin(), aPrivateBrowsingId);
+
+ // Transfer ownership to IPDL.
+ return database.forget();
+}
+
+bool RecvPBackgroundLSDatabaseConstructor(PBackgroundLSDatabaseParent* aActor,
+ const PrincipalInfo& aPrincipalInfo,
+ const uint32_t& aPrivateBrowsingId,
+ const uint64_t& aDatastoreId) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(gPreparedDatastores);
+ MOZ_ASSERT(gPreparedDatastores->Get(aDatastoreId));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+ // The actor is now completely built (it has a manager, channel and it's
+ // registered as a subprotocol).
+ // ActorDestroy will be called if we fail here.
+
+ mozilla::UniquePtr<PreparedDatastore> preparedDatastore;
+ gPreparedDatastores->Remove(aDatastoreId, &preparedDatastore);
+ MOZ_ASSERT(preparedDatastore);
+
+ auto* database = static_cast<Database*>(aActor);
+
+ database->SetActorAlive(&preparedDatastore->MutableDatastoreRef());
+
+ // It's possible that AbortOperationsForLocks was called before the database
+ // actor was created and became live. Let the child know that the database is
+ // no longer valid.
+ if (preparedDatastore->IsInvalidated()) {
+ database->RequestAllowToClose();
+ }
+
+ return true;
+}
+
+PBackgroundLSObserverParent* AllocPBackgroundLSObserverParent(
+ const uint64_t& aObserverId) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!gPreparedObsevers)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ RefPtr<Observer> observer = gPreparedObsevers->Get(aObserverId);
+ if (NS_WARN_IF(!observer)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ // observer->SetObject(this);
+
+ // Transfer ownership to IPDL.
+ return observer.forget().take();
+}
+
+bool RecvPBackgroundLSObserverConstructor(PBackgroundLSObserverParent* aActor,
+ const uint64_t& aObserverId) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(gPreparedObsevers);
+ MOZ_ASSERT(gPreparedObsevers->GetWeak(aObserverId));
+
+ RefPtr<Observer> observer;
+ gPreparedObsevers->Remove(aObserverId, observer.StartAssignment());
+
+ if (!gPreparedObsevers->Count()) {
+ gPreparedObsevers = nullptr;
+ }
+
+ if (!gObservers) {
+ gObservers = new ObserverHashtable();
+ }
+
+ const auto notNullObserver = WrapNotNull(observer.get());
+
+ nsTArray<NotNull<Observer*>>* const array =
+ gObservers->GetOrInsertNew(notNullObserver->Origin());
+ array->AppendElement(notNullObserver);
+
+ if (RefPtr<Datastore> datastore = GetDatastore(observer->Origin())) {
+ datastore->NoteChangedObserverArray(*array);
+ }
+
+ return true;
+}
+
+bool DeallocPBackgroundLSObserverParent(PBackgroundLSObserverParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<Observer> actor = dont_AddRef(static_cast<Observer*>(aActor));
+
+ return true;
+}
+
+PBackgroundLSRequestParent* AllocPBackgroundLSRequestParent(
+ PBackgroundParent* aBackgroundActor, const LSRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != LSRequestParams::T__None);
+
+ if (NS_WARN_IF(!NextGenLocalStorageEnabled())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ Maybe<ContentParentId> contentParentId;
+
+ uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor);
+ if (childID) {
+ contentParentId = Some(ContentParentId(childID));
+ }
+
+ RefPtr<LSRequestBase> actor;
+
+ switch (aParams.type()) {
+ case LSRequestParams::TLSRequestPreloadDatastoreParams:
+ case LSRequestParams::TLSRequestPrepareDatastoreParams: {
+ RefPtr<PrepareDatastoreOp> prepareDatastoreOp =
+ new PrepareDatastoreOp(aParams, contentParentId);
+
+ if (!gPrepareDatastoreOps) {
+ gPrepareDatastoreOps = new PrepareDatastoreOpArray();
+ }
+ gPrepareDatastoreOps->AppendElement(
+ WrapNotNullUnchecked(prepareDatastoreOp.get()));
+
+ actor = std::move(prepareDatastoreOp);
+
+ break;
+ }
+
+ case LSRequestParams::TLSRequestPrepareObserverParams: {
+ RefPtr<PrepareObserverOp> prepareObserverOp =
+ new PrepareObserverOp(aParams, contentParentId);
+
+ actor = std::move(prepareObserverOp);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ // Transfer ownership to IPDL.
+ return actor.forget().take();
+}
+
+bool RecvPBackgroundLSRequestConstructor(PBackgroundLSRequestParent* aActor,
+ const LSRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(aParams.type() != LSRequestParams::T__None);
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+ // The actor is now completely built.
+
+ auto* op = static_cast<LSRequestBase*>(aActor);
+
+ op->Dispatch();
+
+ return true;
+}
+
+bool DeallocPBackgroundLSRequestParent(PBackgroundLSRequestParent* aActor) {
+ AssertIsOnBackgroundThread();
+
+ // Transfer ownership back from IPDL.
+ RefPtr<LSRequestBase> actor =
+ dont_AddRef(static_cast<LSRequestBase*>(aActor));
+
+ return true;
+}
+
+PBackgroundLSSimpleRequestParent* AllocPBackgroundLSSimpleRequestParent(
+ PBackgroundParent* aBackgroundActor, const LSSimpleRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None);
+
+ if (NS_WARN_IF(!NextGenLocalStorageEnabled())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) {
+ return nullptr;
+ }
+
+ Maybe<ContentParentId> contentParentId;
+
+ uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor);
+ if (childID) {
+ contentParentId = Some(ContentParentId(childID));
+ }
+
+ RefPtr<LSSimpleRequestBase> actor;
+
+ switch (aParams.type()) {
+ case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
+ RefPtr<PreloadedOp> preloadedOp =
+ new PreloadedOp(aParams, contentParentId);
+
+ actor = std::move(preloadedOp);
+
+ break;
+ }
+
+ case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: {
+ RefPtr<GetStateOp> getStateOp = new GetStateOp(aParams, contentParentId);
+
+ actor = std::move(getStateOp);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ // Transfer ownership to IPDL.
+ return actor.forget().take();
+}
+
+bool RecvPBackgroundLSSimpleRequestConstructor(
+ PBackgroundLSSimpleRequestParent* aActor,
+ const LSSimpleRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(aParams.type() != LSSimpleRequestParams::T__None);
+ MOZ_ASSERT(NextGenLocalStorageEnabled());
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+
+ // The actor is now completely built.
+
+ auto* op = static_cast<LSSimpleRequestBase*>(aActor);
+
+ op->Dispatch();
+
+ return true;
+}
+
+bool DeallocPBackgroundLSSimpleRequestParent(
+ PBackgroundLSSimpleRequestParent* aActor) {
+ AssertIsOnBackgroundThread();
+
+ // Transfer ownership back from IPDL.
+ RefPtr<LSSimpleRequestBase> actor =
+ dont_AddRef(static_cast<LSSimpleRequestBase*>(aActor));
+
+ return true;
+}
+
+namespace localstorage {
+
+already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+ RefPtr<QuotaClient> client = new QuotaClient();
+ return client.forget();
+}
+
+} // namespace localstorage
+
+/*******************************************************************************
+ * DatastoreWriteOptimizer
+ ******************************************************************************/
+
+void DatastoreWriteOptimizer::ApplyAndReset(
+ nsTArray<LSItemInfo>& aOrderedItems) {
+ AssertIsOnOwningThread();
+
+ // The mWriteInfos hash table contains all write infos, but it keeps them in
+ // an arbitrary order, which means write infos need to be sorted before being
+ // processed. However, the order is not important for deletions and normal
+ // updates. Usually, filtering out deletions and updates would require extra
+ // work, but we have to check the hash table for each ordered item anyway, so
+ // we can remove the write info if it is a deletion or update without adding
+ // extra overhead. In the end, only insertions need to be sorted before being
+ // processed.
+
+ if (mTruncateInfo) {
+ aOrderedItems.Clear();
+ mTruncateInfo = nullptr;
+ }
+
+ for (int32_t index = aOrderedItems.Length() - 1; index >= 0; index--) {
+ LSItemInfo& item = aOrderedItems[index];
+
+ if (auto entry = mWriteInfos.Lookup(item.key())) {
+ WriteInfo* writeInfo = entry->get();
+
+ switch (writeInfo->GetType()) {
+ case WriteInfo::DeleteItem:
+ aOrderedItems.RemoveElementAt(index);
+ entry.Remove();
+ break;
+
+ case WriteInfo::UpdateItem: {
+ auto updateItemInfo = static_cast<UpdateItemInfo*>(writeInfo);
+ if (updateItemInfo->UpdateWithMove()) {
+ // See the comment in LSWriteOptimizer::InsertItem for more details
+ // about the UpdateWithMove flag.
+
+ aOrderedItems.RemoveElementAt(index);
+ entry.Data() = MakeUnique<InsertItemInfo>(
+ updateItemInfo->SerialNumber(), updateItemInfo->GetKey(),
+ updateItemInfo->GetValue());
+ } else {
+ item.value() = updateItemInfo->GetValue();
+ entry.Remove();
+ }
+ break;
+ }
+
+ case WriteInfo::InsertItem:
+ break;
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+ }
+
+ nsTArray<NotNull<WriteInfo*>> writeInfos;
+ GetSortedWriteInfos(writeInfos);
+
+ for (WriteInfo* writeInfo : writeInfos) {
+ MOZ_ASSERT(writeInfo->GetType() == WriteInfo::InsertItem);
+
+ auto insertItemInfo = static_cast<InsertItemInfo*>(writeInfo);
+
+ LSItemInfo* itemInfo = aOrderedItems.AppendElement();
+ itemInfo->key() = insertItemInfo->GetKey();
+ itemInfo->value() = insertItemInfo->GetValue();
+ }
+
+ mWriteInfos.Clear();
+}
+
+/*******************************************************************************
+ * ConnectionWriteOptimizer
+ ******************************************************************************/
+
+Result<int64_t, nsresult> ConnectionWriteOptimizer::Perform(
+ Connection* aConnection, bool aShadowWrites) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ // The order of elements is not stored in the database, so write infos don't
+ // need to be sorted before being processed.
+
+ if (mTruncateInfo) {
+ QM_TRY(MOZ_TO_RESULT(PerformTruncate(aConnection, aShadowWrites)));
+ }
+
+ for (const auto& entry : mWriteInfos) {
+ const WriteInfo* const writeInfo = entry.GetWeak();
+
+ switch (writeInfo->GetType()) {
+ case WriteInfo::InsertItem:
+ case WriteInfo::UpdateItem: {
+ const auto* const insertItemInfo =
+ static_cast<const InsertItemInfo*>(writeInfo);
+
+ QM_TRY(MOZ_TO_RESULT(PerformInsertOrUpdate(
+ aConnection, aShadowWrites, insertItemInfo->GetKey(),
+ insertItemInfo->GetValue())));
+
+ break;
+ }
+
+ case WriteInfo::DeleteItem: {
+ const auto* const deleteItemInfo =
+ static_cast<const DeleteItemInfo*>(writeInfo);
+
+ QM_TRY(MOZ_TO_RESULT(PerformDelete(aConnection, aShadowWrites,
+ deleteItemInfo->GetKey())));
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "UPDATE database "
+ "SET usage = usage + :delta"_ns,
+ [this](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindInt64ByName("delta"_ns, mTotalDelta)));
+
+ return Ok{};
+ })));
+
+ QM_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection->MutableStorageConnection(),
+ "SELECT usage FROM database"_ns));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*stmt, GetInt64, 0));
+}
+
+nsresult ConnectionWriteOptimizer::PerformInsertOrUpdate(
+ Connection* aConnection, bool aShadowWrites, const nsAString& aKey,
+ const LSValue& aValue) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "INSERT OR REPLACE INTO data (key, utf16_length, conversion_type, "
+ "compression_type, value) "
+ "VALUES(:key, :utf16_length, :conversion_type, :compression_type, :value)"_ns,
+ [&aKey, &aValue](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindInt32ByName("utf16_length"_ns, aValue.UTF16Length())));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByName(
+ "conversion_type"_ns,
+ static_cast<int32_t>(aValue.GetConversionType()))));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindInt32ByName(
+ "compression_type"_ns,
+ static_cast<int32_t>(aValue.GetCompressionType()))));
+
+ if (0u == aValue.Length()) { // Otherwise empty string becomes null
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindUTF8StringByName("value"_ns, aValue.AsCString())));
+ } else {
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindUTF8StringAsBlobByName("value"_ns, aValue.AsCString())));
+ }
+
+ return Ok{};
+ })));
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "INSERT OR REPLACE INTO shadow.webappsstore2 "
+ "(originAttributes, originKey, scope, key, value) "
+ "VALUES (:originAttributes, :originKey, :scope, :key, :value) "_ns,
+ [&aConnection, &aKey, &aValue](auto& stmt) -> Result<Ok, nsresult> {
+ using ConversionType = LSValue::ConversionType;
+ using CompressionType = LSValue::CompressionType;
+
+ const ArchivedOriginScope* const archivedOriginScope =
+ aConnection->GetArchivedOriginScope();
+
+ QM_TRY(MOZ_TO_RESULT(archivedOriginScope->BindToStatement(&stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindUTF8StringByName(
+ "scope"_ns, Scheme0Scope(archivedOriginScope->OriginSuffix(),
+ archivedOriginScope->OriginNoSuffix()))));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+
+ bool isCompressed =
+ CompressionType::UNCOMPRESSED != aValue.GetCompressionType();
+ bool isAlreadyConverted =
+ ConversionType::NONE != aValue.GetConversionType();
+
+ nsCString buffer;
+ const nsCString& valueBlob = aValue.AsCString();
+ if (isCompressed) {
+ QM_TRY(OkIf(SnappyUncompress(valueBlob, buffer)),
+ Err(NS_ERROR_FAILURE));
+ }
+ const nsCString& value = isCompressed ? buffer : valueBlob;
+
+ // For shadow writes, we undo buffer swap and convert destructively
+ nsCString unconverted;
+ if (!isAlreadyConverted) {
+ nsString converted;
+ QM_TRY(OkIf(PutCStringBytesToString(value, converted)),
+ Err(NS_ERROR_OUT_OF_MEMORY));
+ QM_TRY(OkIf(CopyUTF16toUTF8(converted, unconverted, fallible)),
+ Err(NS_ERROR_OUT_OF_MEMORY)); // Corrupt invalid data
+ }
+ const nsCString& untransformed =
+ (!isAlreadyConverted) ? unconverted : value;
+
+ QM_TRY(MOZ_TO_RESULT(
+ stmt.BindUTF8StringByName("value"_ns, untransformed)));
+
+ return Ok{};
+ })));
+
+ return NS_OK;
+}
+
+nsresult ConnectionWriteOptimizer::PerformDelete(Connection* aConnection,
+ bool aShadowWrites,
+ const nsAString& aKey) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "DELETE FROM data "
+ "WHERE key = :key;"_ns,
+ [&aKey](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+
+ return Ok{};
+ })));
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "DELETE FROM shadow.webappsstore2 "
+ "WHERE originAttributes = :originAttributes "
+ "AND originKey = :originKey "
+ "AND key = :key;"_ns,
+ [&aConnection, &aKey](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->GetArchivedOriginScope()->BindToStatement(&stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt.BindStringByName("key"_ns, aKey)));
+
+ return Ok{};
+ })));
+
+ return NS_OK;
+}
+
+nsresult ConnectionWriteOptimizer::PerformTruncate(Connection* aConnection,
+ bool aShadowWrites) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteCachedStatement("DELETE FROM data;"_ns)));
+
+ if (!aShadowWrites) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteCachedStatement(
+ "DELETE FROM shadow.webappsstore2 "
+ "WHERE originAttributes = :originAttributes "
+ "AND originKey = :originKey;"_ns,
+ [&aConnection](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->GetArchivedOriginScope()->BindToStatement(&stmt)));
+
+ return Ok{};
+ })));
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * DatastoreOperationBase
+ ******************************************************************************/
+
+/*******************************************************************************
+ * ConnectionDatastoreOperationBase
+ ******************************************************************************/
+
+ConnectionDatastoreOperationBase::ConnectionDatastoreOperationBase(
+ Connection* aConnection, bool aEnsureStorageConnection)
+ : mConnection(aConnection),
+ mEnsureStorageConnection(aEnsureStorageConnection) {
+ MOZ_ASSERT(aConnection);
+}
+
+ConnectionDatastoreOperationBase::~ConnectionDatastoreOperationBase() {
+ MOZ_ASSERT(!mConnection,
+ "ConnectionDatabaseOperationBase::Cleanup() was not called by a "
+ "subclass!");
+}
+
+void ConnectionDatastoreOperationBase::Cleanup() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnection);
+
+ mConnection = nullptr;
+
+ NoteComplete();
+}
+
+void ConnectionDatastoreOperationBase::OnSuccess() { AssertIsOnOwningThread(); }
+
+void ConnectionDatastoreOperationBase::OnFailure(nsresult aResultCode) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NS_FAILED(aResultCode));
+}
+
+void ConnectionDatastoreOperationBase::RunOnConnectionThread() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+
+ if (!MayProceedOnNonOwningThread()) {
+ SetFailureCode(NS_ERROR_ABORT);
+ } else {
+ nsresult rv = NS_OK;
+
+ // The boolean flag is only used by the CloseOp to avoid creating empty
+ // databases.
+ if (mEnsureStorageConnection) {
+ rv = mConnection->EnsureStorageConnection();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ SetFailureCode(rv);
+ } else {
+ MOZ_ASSERT(mConnection->HasStorageConnection());
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = DoDatastoreWork();
+ if (NS_FAILED(rv)) {
+ SetFailureCode(rv);
+ }
+ }
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+void ConnectionDatastoreOperationBase::RunOnOwningThread() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnection);
+
+ if (!MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ OnSuccess();
+ } else {
+ OnFailure(ResultCode());
+ }
+
+ Cleanup();
+}
+
+NS_IMETHODIMP
+ConnectionDatastoreOperationBase::Run() {
+ if (IsOnGlobalConnectionThread()) {
+ RunOnConnectionThread();
+ } else {
+ RunOnOwningThread();
+ }
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * Connection implementation
+ ******************************************************************************/
+
+Connection::Connection(ConnectionThread* aConnectionThread,
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable)
+ : mConnectionThread(aConnectionThread),
+ mQuotaClient(QuotaClient::GetInstance()),
+ mArchivedOriginScope(std::move(aArchivedOriginScope)),
+ mOriginMetadata(aOriginMetadata),
+ mDatabaseWasNotAvailable(aDatabaseWasNotAvailable),
+ mHasCreatedDatabase(false),
+ mFlushScheduled(false)
+#ifdef DEBUG
+ ,
+ mInUpdateBatch(false),
+ mFinished(false)
+#endif
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!aOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+}
+
+Connection::~Connection() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mFlushScheduled);
+ MOZ_ASSERT(!mInUpdateBatch);
+ MOZ_ASSERT(mFinished);
+}
+
+void Connection::Dispatch(ConnectionDatastoreOperationBase* aOp) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnectionThread);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ mConnectionThread->mThread->Dispatch(aOp, NS_DISPATCH_NORMAL));
+}
+
+void Connection::Close(nsIRunnable* aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+
+ if (mFlushScheduled) {
+ MOZ_ASSERT(mFlushTimer);
+ MOZ_ALWAYS_SUCCEEDS(mFlushTimer->Cancel());
+
+ Flush();
+
+ mFlushTimer = nullptr;
+ }
+
+ RefPtr<CloseOp> op = new CloseOp(this, aCallback);
+
+ Dispatch(op);
+}
+
+void Connection::SetItem(const nsString& aKey, const LSValue& aValue,
+ int64_t aDelta, bool aIsNewItem) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ if (aIsNewItem) {
+ mWriteOptimizer.InsertItem(aKey, aValue, aDelta);
+ } else {
+ mWriteOptimizer.UpdateItem(aKey, aValue, aDelta);
+ }
+}
+
+void Connection::RemoveItem(const nsString& aKey, int64_t aDelta) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ mWriteOptimizer.DeleteItem(aKey, aDelta);
+}
+
+void Connection::Clear(int64_t aDelta) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ mWriteOptimizer.Truncate(aDelta);
+}
+
+void Connection::BeginUpdateBatch() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mInUpdateBatch);
+
+#ifdef DEBUG
+ mInUpdateBatch = true;
+#endif
+}
+
+void Connection::EndUpdateBatch() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mInUpdateBatch);
+
+ if (mWriteOptimizer.HasWrites() && !mFlushScheduled) {
+ ScheduleFlush();
+ }
+
+#ifdef DEBUG
+ mInUpdateBatch = false;
+#endif
+}
+
+nsresult Connection::EnsureStorageConnection() {
+ AssertIsOnGlobalConnectionThread();
+
+ if (HasStorageConnection()) {
+ return NS_OK;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ if (!mDatabaseWasNotAvailable || mHasCreatedDatabase) {
+ MOZ_ASSERT(mOriginMetadata.mPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ QM_TRY_INSPECT(const auto& directoryEntry,
+ quotaManager->GetOriginDirectory(mOriginMetadata));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDirectoryPath)));
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName)));
+
+ QM_TRY_INSPECT(
+ const auto& databaseFilePath,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directoryEntry, GetPath));
+
+ QM_TRY_UNWRAP(auto storageConnection,
+ GetStorageConnection(databaseFilePath));
+ LazyInit(WrapMovingNotNull(std::move(storageConnection)));
+
+ return NS_OK;
+ }
+
+ RefPtr<InitTemporaryOriginHelper> helper =
+ new InitTemporaryOriginHelper(mOriginMetadata);
+
+ QM_TRY_INSPECT(const auto& originDirectoryPath,
+ helper->BlockAndReturnOriginDirectoryPath());
+
+ QM_TRY_INSPECT(const auto& directoryEntry,
+ QM_NewLocalFile(originDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDirectoryPath)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directoryEntry, Exists));
+
+ if (!exists) {
+ QM_TRY(
+ MOZ_TO_RESULT(directoryEntry->Create(nsIFile::DIRECTORY_TYPE, 0755)));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directoryEntry, Exists));
+
+ MOZ_ASSERT(!exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(mDirectoryPath));
+
+ nsCOMPtr<mozIStorageConnection> storageConnection;
+
+ auto autoRemove = MakeScopeExit([&storageConnection, &directoryEntry] {
+ if (storageConnection) {
+ MOZ_ALWAYS_SUCCEEDS(storageConnection->Close());
+ }
+
+ nsresult rv = directoryEntry->Remove(false);
+ if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
+ NS_WARNING("Failed to remove database file!");
+ }
+ });
+
+ QM_TRY_UNWRAP(storageConnection, CreateStorageConnectionWithRecovery(
+ *directoryEntry, *usageFile, Origin(),
+ [] { MOZ_ASSERT_UNREACHABLE(); }));
+
+ MOZ_ASSERT(mQuotaClient);
+
+ MutexAutoLock shadowDatabaseLock(mQuotaClient->ShadowDatabaseMutex());
+
+ nsCOMPtr<mozIStorageConnection> shadowConnection;
+ if (!gInitializedShadowStorage) {
+ QM_TRY_UNWRAP(shadowConnection,
+ CreateShadowStorageConnection(quotaManager->GetBasePath()));
+
+ gInitializedShadowStorage = true;
+ }
+
+ autoRemove.release();
+
+ if (!mHasCreatedDatabase) {
+ mHasCreatedDatabase = true;
+ }
+
+ LazyInit(WrapMovingNotNull(std::move(storageConnection)));
+
+ return NS_OK;
+}
+
+void Connection::CloseStorageConnection() {
+ AssertIsOnGlobalConnectionThread();
+
+ CachingDatabaseConnection::Close();
+}
+
+nsresult Connection::BeginWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("BEGIN IMMEDIATE;"_ns)));
+
+ return NS_OK;
+}
+
+nsresult Connection::CommitWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ QM_TRY(MOZ_TO_RESULT(ExecuteCachedStatement("COMMIT;"_ns)));
+
+ return NS_OK;
+}
+
+nsresult Connection::RollbackWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement("ROLLBACK;"_ns));
+
+ // This may fail if SQLite already rolled back the transaction so ignore any
+ // errors.
+ Unused << stmt->Execute();
+
+ return NS_OK;
+}
+
+void Connection::ScheduleFlush() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mWriteOptimizer.HasWrites());
+ MOZ_ASSERT(!mFlushScheduled);
+
+ if (!mFlushTimer) {
+ mFlushTimer = NS_NewTimer();
+ MOZ_ASSERT(mFlushTimer);
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(mFlushTimer->InitWithNamedFuncCallback(
+ FlushTimerCallback, this, kFlushTimeoutMs, nsITimer::TYPE_ONE_SHOT,
+ "Connection::FlushTimerCallback"));
+
+ mFlushScheduled = true;
+}
+
+void Connection::Flush() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mFlushScheduled);
+
+ if (mWriteOptimizer.HasWrites()) {
+ RefPtr<FlushOp> op = new FlushOp(this, std::move(mWriteOptimizer));
+
+ Dispatch(op);
+ }
+
+ mFlushScheduled = false;
+}
+
+// static
+void Connection::FlushTimerCallback(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(aClosure);
+
+ auto* self = static_cast<Connection*>(aClosure);
+ MOZ_ASSERT(self);
+ MOZ_ASSERT(self->mFlushScheduled);
+
+ self->Flush();
+}
+
+Result<nsString, nsresult>
+Connection::InitTemporaryOriginHelper::BlockAndReturnOriginDirectoryPath() {
+ AssertIsOnGlobalConnectionThread();
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ mozilla::MonitorAutoLock lock(mMonitor);
+ while (mWaiting) {
+ lock.Wait();
+ }
+
+ QM_TRY(MOZ_TO_RESULT(mIOThreadResultCode));
+
+ return mOriginDirectoryPath;
+}
+
+nsresult Connection::InitTemporaryOriginHelper::RunOnIOThread() {
+ AssertIsOnIOThread();
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_INSPECT(const auto& directoryEntry,
+ quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata)
+ .map([](const auto& res) { return res.first; }));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mOriginDirectoryPath)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Connection::InitTemporaryOriginHelper::Run() {
+ AssertIsOnIOThread();
+
+ nsresult rv = RunOnIOThread();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mIOThreadResultCode = rv;
+ }
+
+ mozilla::MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mWaiting);
+
+ mWaiting = false;
+ lock.Notify();
+
+ return NS_OK;
+}
+
+Connection::FlushOp::FlushOp(Connection* aConnection,
+ ConnectionWriteOptimizer&& aWriteOptimizer)
+ : ConnectionDatastoreOperationBase(aConnection),
+ mWriteOptimizer(std::move(aWriteOptimizer)),
+ mShadowWrites(gShadowWrites) {}
+
+nsresult Connection::FlushOp::DoDatastoreWork() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ AutoWriteTransaction autoWriteTransaction(mShadowWrites);
+
+ QM_TRY(MOZ_TO_RESULT(autoWriteTransaction.Start(mConnection)));
+
+ QM_TRY_INSPECT(const int64_t& usage,
+ mWriteOptimizer.Perform(mConnection, mShadowWrites));
+
+ QM_TRY_INSPECT(const auto& usageFile,
+ GetUsageFile(mConnection->DirectoryPath()));
+
+ QM_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(mConnection->DirectoryPath()));
+
+ QM_TRY(MOZ_TO_RESULT(UpdateUsageFile(usageFile, usageJournalFile, usage)));
+
+ QM_TRY(MOZ_TO_RESULT(autoWriteTransaction.Commit()));
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+
+ return NS_OK;
+}
+
+void Connection::FlushOp::Cleanup() {
+ AssertIsOnOwningThread();
+
+ mWriteOptimizer.Reset();
+
+ MOZ_ASSERT(!mWriteOptimizer.HasWrites());
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+nsresult Connection::CloseOp::DoDatastoreWork() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ if (mConnection->HasStorageConnection()) {
+ mConnection->CloseStorageConnection();
+ }
+
+ return NS_OK;
+}
+
+void Connection::CloseOp::Cleanup() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mConnection);
+
+ mConnection->mConnectionThread->mConnections.Remove(mConnection->Origin());
+
+#ifdef DEBUG
+ MOZ_ASSERT(!mConnection->mFinished);
+ mConnection->mFinished = true;
+#endif
+
+ nsCOMPtr<nsIRunnable> callback;
+ mCallback.swap(callback);
+
+ callback->Run();
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+/*******************************************************************************
+ * ConnectionThread implementation
+ ******************************************************************************/
+
+ConnectionThread::ConnectionThread() {
+ AssertIsOnOwningThread();
+ AssertIsOnBackgroundThread();
+
+ MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("LS Thread", getter_AddRefs(mThread)));
+}
+
+ConnectionThread::~ConnectionThread() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!mConnections.Count());
+}
+
+bool ConnectionThread::IsOnConnectionThread() {
+ MOZ_ASSERT(mThread);
+
+ bool current;
+ return NS_SUCCEEDED(mThread->IsOnCurrentThread(&current)) && current;
+}
+
+void ConnectionThread::AssertIsOnConnectionThread() {
+ MOZ_ASSERT(IsOnConnectionThread());
+}
+
+already_AddRefed<Connection> ConnectionThread::CreateConnection(
+ const OriginMetadata& aOriginMetadata,
+ UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope,
+ bool aDatabaseWasNotAvailable) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+ MOZ_ASSERT(!mConnections.Contains(aOriginMetadata.mOrigin));
+
+ RefPtr<Connection> connection =
+ new Connection(this, aOriginMetadata, std::move(aArchivedOriginScope),
+ aDatabaseWasNotAvailable);
+ mConnections.InsertOrUpdate(aOriginMetadata.mOrigin, RefPtr{connection});
+
+ return connection.forget();
+}
+
+void ConnectionThread::Shutdown() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mThread);
+
+ mThread->Shutdown();
+}
+
+/*******************************************************************************
+ * Datastore
+ ******************************************************************************/
+
+Datastore::Datastore(const OriginMetadata& aOriginMetadata,
+ uint32_t aPrivateBrowsingId, int64_t aUsage,
+ int64_t aSizeOfKeys, int64_t aSizeOfItems,
+ RefPtr<DirectoryLock>&& aDirectoryLock,
+ RefPtr<Connection>&& aConnection,
+ RefPtr<QuotaObject>&& aQuotaObject,
+ nsTHashMap<nsStringHashKey, LSValue>& aValues,
+ nsTArray<LSItemInfo>&& aOrderedItems)
+ : mDirectoryLock(std::move(aDirectoryLock)),
+ mConnection(std::move(aConnection)),
+ mQuotaObject(std::move(aQuotaObject)),
+ mOrderedItems(std::move(aOrderedItems)),
+ mOriginMetadata(aOriginMetadata),
+ mPrivateBrowsingId(aPrivateBrowsingId),
+ mUsage(aUsage),
+ mUpdateBatchUsage(-1),
+ mSizeOfKeys(aSizeOfKeys),
+ mSizeOfItems(aSizeOfItems),
+ mClosed(false),
+ mInUpdateBatch(false),
+ mHasLivePrivateDatastore(false) {
+ AssertIsOnBackgroundThread();
+
+ mValues.SwapElements(aValues);
+}
+
+Datastore::~Datastore() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mClosed);
+}
+
+void Datastore::Close() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(!mPrepareDatastoreOps.Count());
+ MOZ_ASSERT(!mPreparedDatastores.Count());
+ MOZ_ASSERT(!mDatabases.Count());
+ MOZ_ASSERT(mDirectoryLock);
+
+ mClosed = true;
+
+ if (IsPersistent()) {
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mQuotaObject);
+
+ // We can't release the directory lock and unregister itself from the
+ // hashtable until the connection is fully closed.
+ nsCOMPtr<nsIRunnable> callback =
+ NewRunnableMethod("dom::Datastore::ConnectionClosedCallback", this,
+ &Datastore::ConnectionClosedCallback);
+ mConnection->Close(callback);
+ } else {
+ MOZ_ASSERT(!mConnection);
+ MOZ_ASSERT(!mQuotaObject);
+
+ // There's no connection, so it's safe to release the directory lock and
+ // unregister itself from the hashtable.
+
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+ }
+}
+
+void Datastore::WaitForConnectionToComplete(nsIRunnable* aCallback) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!mCompleteCallback);
+ MOZ_ASSERT(mClosed);
+
+ mCompleteCallback = aCallback;
+}
+
+void Datastore::NoteLivePrepareDatastoreOp(
+ PrepareDatastoreOp* aPrepareDatastoreOp) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPrepareDatastoreOp);
+ MOZ_ASSERT(!mPrepareDatastoreOps.Contains(aPrepareDatastoreOp));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPrepareDatastoreOps.Insert(aPrepareDatastoreOp);
+}
+
+void Datastore::NoteFinishedPrepareDatastoreOp(
+ PrepareDatastoreOp* aPrepareDatastoreOp) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOps.Contains(aPrepareDatastoreOp));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPrepareDatastoreOps.Remove(aPrepareDatastoreOp);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PrepareDatastoreOp finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteLivePrivateDatastore() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mHasLivePrivateDatastore);
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mHasLivePrivateDatastore = true;
+}
+
+void Datastore::NoteFinishedPrivateDatastore() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mHasLivePrivateDatastore);
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mHasLivePrivateDatastore = false;
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PrivateDatastore finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteLivePreparedDatastore(
+ PreparedDatastore* aPreparedDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPreparedDatastore);
+ MOZ_ASSERT(!mPreparedDatastores.Contains(aPreparedDatastore));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPreparedDatastores.Insert(aPreparedDatastore);
+}
+
+void Datastore::NoteFinishedPreparedDatastore(
+ PreparedDatastore* aPreparedDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aPreparedDatastore);
+ MOZ_ASSERT(mPreparedDatastores.Contains(aPreparedDatastore));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mPreparedDatastores.Remove(aPreparedDatastore);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PreparedDatastore finished"_ns);
+
+ MaybeClose();
+}
+
+bool Datastore::HasOtherProcessDatabases(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+
+ PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+ for (Database* database : mDatabases) {
+ if (database->Manager() != databaseBackgroundActor) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Datastore::NoteLiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mDatabases.Insert(aDatabase);
+
+ NoteChangedDatabaseMap();
+}
+
+void Datastore::NoteFinishedDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mActiveDatabases.Contains(aDatabase));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(!mClosed);
+
+ mDatabases.Remove(aDatabase);
+
+ NoteChangedDatabaseMap();
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS,
+ "Database finished"_ns);
+
+ MaybeClose();
+}
+
+void Datastore::NoteActiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mActiveDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mClosed);
+
+ mActiveDatabases.Insert(aDatabase);
+}
+
+void Datastore::NoteInactiveDatabase(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(mDatabases.Contains(aDatabase));
+ MOZ_ASSERT(mActiveDatabases.Contains(aDatabase));
+ MOZ_ASSERT(!mClosed);
+
+ mActiveDatabases.Remove(aDatabase);
+
+ if (!mActiveDatabases.Count() && mPendingUsageDeltas.Length()) {
+ int64_t finalDelta = 0;
+
+ for (auto delta : mPendingUsageDeltas) {
+ finalDelta += delta;
+ }
+
+ MOZ_ASSERT(finalDelta <= 0);
+
+ if (finalDelta != 0) {
+ DebugOnly<bool> ok = UpdateUsage(finalDelta);
+ MOZ_ASSERT(ok);
+ }
+
+ mPendingUsageDeltas.Clear();
+ }
+}
+
+void Datastore::GetSnapshotLoadInfo(const nsAString& aKey,
+ bool& aAddKeyToUnknownItems,
+ nsTHashtable<nsStringHashKey>& aLoadedItems,
+ nsTArray<LSItemInfo>& aItemInfos,
+ uint32_t& aNextLoadIndex,
+ LSSnapshot::LoadState& aLoadState) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(!mInUpdateBatch);
+
+#ifdef DEBUG
+ int64_t sizeOfKeys = 0;
+ int64_t sizeOfItems = 0;
+ for (auto item : mOrderedItems) {
+ int64_t sizeOfKey = static_cast<int64_t>(item.key().Length());
+ sizeOfKeys += sizeOfKey;
+ sizeOfItems += sizeOfKey + static_cast<int64_t>(item.value().Length());
+ }
+ MOZ_ASSERT(mSizeOfKeys == sizeOfKeys);
+ MOZ_ASSERT(mSizeOfItems == sizeOfItems);
+#endif
+
+ // Computes load state optimized for current size of keys and items.
+ // Zero key length and value can be passed to do a quick initial estimation.
+ // If computed load state is already AllOrderedItems then excluded key length
+ // and value length can't make it any better.
+ auto GetLoadState = [&](int64_t aKeyLength, int64_t aValueLength) {
+ if (mSizeOfKeys - aKeyLength <= gSnapshotPrefill) {
+ if (mSizeOfItems - aKeyLength - aValueLength <= gSnapshotPrefill) {
+ return LSSnapshot::LoadState::AllOrderedItems;
+ }
+
+ return LSSnapshot::LoadState::AllOrderedKeys;
+ }
+
+ return LSSnapshot::LoadState::Partial;
+ };
+
+ // Value for given aKey if aKey is not void (can be void too if value doesn't
+ // exist for given aKey).
+ LSValue value;
+ // If aKey and value are not void, checkKey will be set to true. Once we find
+ // an item for given aKey in one of the loops below, checkKey is set to false
+ // to prevent additional comparison of strings (string implementation compares
+ // string lengths first to avoid char by char comparison if possible).
+ bool checkKey = false;
+
+ // Avoid additional hash lookup if all ordered items fit into initial prefill
+ // already.
+ LSSnapshot::LoadState loadState = GetLoadState(/* aKeyLength */ 0,
+ /* aValueLength */ 0);
+ if (loadState != LSSnapshot::LoadState::AllOrderedItems && !aKey.IsVoid()) {
+ GetItem(aKey, value);
+ if (!value.IsVoid()) {
+ // Ok, we have a non void aKey and value.
+
+ // We have to watch for aKey during one of the loops below to exclude it
+ // from the size computation. The super fast mode (AllOrderedItems)
+ // doesn't have to do that though.
+ checkKey = true;
+
+ // We have to compute load state again because aKey length and value
+ // length is excluded from the size in this case.
+ loadState = GetLoadState(aKey.Length(), value.Length());
+ }
+ }
+
+ switch (loadState) {
+ case LSSnapshot::LoadState::AllOrderedItems: {
+ // We're sending all ordered items, we don't need to check keys because
+ // mOrderedItems must contain a value for aKey if checkKey is true.
+
+ aItemInfos.AppendElements(mOrderedItems);
+
+ MOZ_ASSERT(aItemInfos.Length() == mValues.Count());
+ aNextLoadIndex = mValues.Count();
+
+ aAddKeyToUnknownItems = false;
+
+ break;
+ }
+
+ case LSSnapshot::LoadState::AllOrderedKeys: {
+ // We don't have enough snapshot budget to send all items, but we do have
+ // enough to send all of the keys and to make a best effort to populate as
+ // many values as possible. We send void string values once we run out of
+ // budget. A complicating factor is that we want to make sure that we send
+ // the value for aKey which is a localStorage read that's triggering this
+ // request. Since that key can happen anywhere in the list of items, we
+ // need to handle it specially.
+ //
+ // The loop is effectively doing 2 things in parallel:
+ //
+ // 1. Looking for the `aKey` to send. This is tracked by `checkKey`
+ // which is true if there was an `aKey` specified and until we
+ // populate its value, and false thereafter.
+ // 2. Sending values until we run out of `size` budget and switch to
+ // sending void values. `doneSendingValues` tracks when we've run out
+ // of size budget, with `setVoidValue` tracking whether a value
+ // should be sent for each turn of the event loop but can be
+ // overridden when `aKey` is found.
+
+ int64_t size = mSizeOfKeys;
+ bool setVoidValue = false;
+ bool doneSendingValues = false;
+ for (uint32_t index = 0; index < mOrderedItems.Length(); index++) {
+ const LSItemInfo& item = mOrderedItems[index];
+
+ const nsString& key = item.key();
+ const LSValue& value = item.value();
+
+ if (checkKey && key == aKey) {
+ checkKey = false;
+ setVoidValue = false;
+ } else if (!setVoidValue) {
+ if (doneSendingValues) {
+ setVoidValue = true;
+ } else {
+ size += static_cast<int64_t>(value.Length());
+
+ if (size > gSnapshotPrefill) {
+ setVoidValue = true;
+ doneSendingValues = true;
+
+ // We set doneSendingValues to true and that will guard against
+ // entering this branch during next iterations. So aNextLoadIndex
+ // is set only once.
+ aNextLoadIndex = index;
+ }
+ }
+ }
+
+ LSItemInfo* itemInfo = aItemInfos.AppendElement();
+ itemInfo->key() = key;
+ if (setVoidValue) {
+ itemInfo->value().SetIsVoid(true);
+ } else {
+ aLoadedItems.PutEntry(key);
+ itemInfo->value() = value;
+ }
+ }
+
+ aAddKeyToUnknownItems = false;
+
+ break;
+ }
+
+ case LSSnapshot::LoadState::Partial: {
+ int64_t size = 0;
+ for (uint32_t index = 0; index < mOrderedItems.Length(); index++) {
+ const LSItemInfo& item = mOrderedItems[index];
+
+ const nsString& key = item.key();
+ const LSValue& value = item.value();
+
+ if (checkKey && key == aKey) {
+ checkKey = false;
+ } else {
+ size += static_cast<int64_t>(key.Length()) +
+ static_cast<int64_t>(value.Length());
+
+ if (size > gSnapshotPrefill) {
+ aNextLoadIndex = index;
+ break;
+ }
+ }
+
+ aLoadedItems.PutEntry(key);
+
+ LSItemInfo* itemInfo = aItemInfos.AppendElement();
+ itemInfo->key() = key;
+ itemInfo->value() = value;
+ }
+
+ aAddKeyToUnknownItems = false;
+
+ if (!aKey.IsVoid()) {
+ if (value.IsVoid()) {
+ aAddKeyToUnknownItems = true;
+ } else if (checkKey) {
+ // The item wasn't added in the loop above, add it here.
+
+ LSItemInfo* itemInfo = aItemInfos.AppendElement();
+ itemInfo->key() = aKey;
+ itemInfo->value() = value;
+ }
+ }
+
+ MOZ_ASSERT(aItemInfos.Length() < mOrderedItems.Length());
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad load state value!");
+ }
+
+ aLoadState = loadState;
+}
+
+void Datastore::GetItem(const nsAString& aKey, LSValue& aValue) const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+
+ if (!mValues.Get(aKey, &aValue)) {
+ aValue.SetIsVoid(true);
+ }
+}
+
+void Datastore::GetKeys(nsTArray<nsString>& aKeys) const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+
+ for (auto item : mOrderedItems) {
+ aKeys.AppendElement(item.key());
+ }
+}
+
+void Datastore::SetItem(Database* aDatabase, const nsString& aKey,
+ const LSValue& aValue) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mInUpdateBatch);
+
+ LSValue oldValue;
+ GetItem(aKey, oldValue);
+
+ if (oldValue != aValue) {
+ bool isNewItem = oldValue.IsVoid();
+
+ NotifySnapshots(aDatabase, aKey, oldValue, /* affectsOrder */ isNewItem);
+
+ mValues.InsertOrUpdate(aKey, aValue);
+
+ int64_t delta;
+
+ if (isNewItem) {
+ mWriteOptimizer.InsertItem(aKey, aValue);
+
+ int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
+
+ delta = sizeOfKey + static_cast<int64_t>(aValue.UTF16Length());
+
+ mUpdateBatchUsage += delta;
+
+ mSizeOfKeys += sizeOfKey;
+ mSizeOfItems += sizeOfKey + static_cast<int64_t>(aValue.Length());
+ } else {
+ mWriteOptimizer.UpdateItem(aKey, aValue);
+
+ delta = static_cast<int64_t>(aValue.UTF16Length()) -
+ static_cast<int64_t>(oldValue.UTF16Length());
+
+ mUpdateBatchUsage += delta;
+
+ mSizeOfItems += static_cast<int64_t>(aValue.Length()) -
+ static_cast<int64_t>(oldValue.Length());
+ }
+
+ if (IsPersistent()) {
+ mConnection->SetItem(aKey, aValue, delta, isNewItem);
+ }
+ }
+}
+
+void Datastore::RemoveItem(Database* aDatabase, const nsString& aKey) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mInUpdateBatch);
+
+ LSValue oldValue;
+ GetItem(aKey, oldValue);
+
+ if (!oldValue.IsVoid()) {
+ NotifySnapshots(aDatabase, aKey, oldValue, /* aAffectsOrder */ true);
+
+ mValues.Remove(aKey);
+
+ mWriteOptimizer.DeleteItem(aKey);
+
+ int64_t sizeOfKey = static_cast<int64_t>(aKey.Length());
+
+ int64_t delta = -sizeOfKey - static_cast<int64_t>(oldValue.UTF16Length());
+
+ mUpdateBatchUsage += delta;
+
+ mSizeOfKeys -= sizeOfKey;
+ mSizeOfItems -= sizeOfKey + static_cast<int64_t>(oldValue.Length());
+
+ if (IsPersistent()) {
+ mConnection->RemoveItem(aKey, delta);
+ }
+ }
+}
+
+void Datastore::Clear(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+
+ if (mValues.Count()) {
+ int64_t delta = 0;
+ for (const auto& entry : mValues) {
+ const nsAString& key = entry.GetKey();
+ const LSValue& value = entry.GetData();
+
+ delta += -static_cast<int64_t>(key.Length()) -
+ static_cast<int64_t>(value.UTF16Length());
+
+ NotifySnapshots(aDatabase, key, value, /* aAffectsOrder */ true);
+ }
+
+ mValues.Clear();
+
+ if (mInUpdateBatch) {
+ mWriteOptimizer.Truncate();
+
+ mUpdateBatchUsage += delta;
+ } else {
+ mOrderedItems.Clear();
+
+ DebugOnly<bool> ok = UpdateUsage(delta);
+ MOZ_ASSERT(ok);
+ }
+
+ mSizeOfKeys = 0;
+ mSizeOfItems = 0;
+
+ if (IsPersistent()) {
+ mConnection->Clear(delta);
+ }
+ }
+}
+
+void Datastore::BeginUpdateBatch(int64_t aSnapshotUsage) {
+ AssertIsOnBackgroundThread();
+ // Don't assert `aSnapshotUsage >= 0`, it can be negative when multiple
+ // snapshots are operating in parallel.
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mUpdateBatchUsage == -1);
+ MOZ_ASSERT(!mInUpdateBatch);
+
+ mUpdateBatchUsage = aSnapshotUsage;
+
+ if (IsPersistent()) {
+ mConnection->BeginUpdateBatch();
+ }
+
+ mInUpdateBatch = true;
+}
+
+int64_t Datastore::EndUpdateBatch(int64_t aSnapshotPeakUsage) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mClosed);
+ MOZ_ASSERT(mInUpdateBatch);
+
+ mWriteOptimizer.ApplyAndReset(mOrderedItems);
+
+ MOZ_ASSERT(!mWriteOptimizer.HasWrites());
+
+ if (aSnapshotPeakUsage >= 0) {
+ int64_t delta = mUpdateBatchUsage - aSnapshotPeakUsage;
+
+ if (mActiveDatabases.Count()) {
+ // We can't apply deltas while other databases are still active.
+ // The final delta must be zero or negative, but individual deltas can
+ // be positive. A positive delta can't be applied asynchronously since
+ // there's no way to fire the quota exceeded error event.
+
+ mPendingUsageDeltas.AppendElement(delta);
+ } else {
+ MOZ_ASSERT(delta <= 0);
+ if (delta != 0) {
+ DebugOnly<bool> ok = UpdateUsage(delta);
+ MOZ_ASSERT(ok);
+ }
+ }
+ }
+
+ int64_t result = mUpdateBatchUsage;
+ mUpdateBatchUsage = -1;
+
+ if (IsPersistent()) {
+ mConnection->EndUpdateBatch();
+ }
+
+ mInUpdateBatch = false;
+
+ return result;
+}
+
+int64_t Datastore::AttemptToUpdateUsage(int64_t aMinSize, bool aInitial) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT_IF(aInitial, aMinSize >= 0);
+ MOZ_ASSERT_IF(!aInitial, aMinSize > 0);
+
+ const int64_t size = aMinSize + GetSnapshotPeakUsagePreincrement(aInitial);
+
+ if (size && UpdateUsage(size)) {
+ return size;
+ }
+
+ const int64_t reducedSize =
+ aMinSize + GetSnapshotPeakUsageReducedPreincrement(aInitial);
+
+ if (reducedSize && UpdateUsage(reducedSize)) {
+ return reducedSize;
+ }
+
+ if (aMinSize > 0 && UpdateUsage(aMinSize)) {
+ return aMinSize;
+ }
+
+ return 0;
+}
+
+bool Datastore::HasOtherProcessObservers(Database* aDatabase) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+
+ if (!gObservers) {
+ return false;
+ }
+
+ nsTArray<NotNull<Observer*>>* array;
+ if (!gObservers->Get(mOriginMetadata.mOrigin, &array)) {
+ return false;
+ }
+
+ MOZ_ASSERT(array);
+
+ PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+ for (Observer* observer : *array) {
+ if (observer->Manager() != databaseBackgroundActor) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void Datastore::NotifyOtherProcessObservers(Database* aDatabase,
+ const nsString& aDocumentURI,
+ const nsString& aKey,
+ const LSValue& aOldValue,
+ const LSValue& aNewValue) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+
+ if (!gObservers) {
+ return;
+ }
+
+ nsTArray<NotNull<Observer*>>* array;
+ if (!gObservers->Get(mOriginMetadata.mOrigin, &array)) {
+ return;
+ }
+
+ MOZ_ASSERT(array);
+
+ // We do not want to send information about events back to the content process
+ // that caused the change.
+ PBackgroundParent* databaseBackgroundActor = aDatabase->Manager();
+
+ for (Observer* observer : *array) {
+ if (observer->Manager() != databaseBackgroundActor) {
+ observer->Observe(aDatabase, aDocumentURI, aKey, aOldValue, aNewValue);
+ }
+ }
+}
+
+void Datastore::NoteChangedObserverArray(
+ const nsTArray<NotNull<Observer*>>& aObservers) {
+ AssertIsOnBackgroundThread();
+
+ for (Database* database : mActiveDatabases) {
+ Snapshot* snapshot = database->GetSnapshot();
+ MOZ_ASSERT(snapshot);
+
+ if (snapshot->IsDirty()) {
+ continue;
+ }
+
+ bool hasOtherProcessObservers = false;
+
+ PBackgroundParent* databaseBackgroundActor = database->Manager();
+
+ for (Observer* observer : aObservers) {
+ if (observer->Manager() != databaseBackgroundActor) {
+ hasOtherProcessObservers = true;
+ break;
+ }
+ }
+
+ if (snapshot->HasOtherProcessObservers() != hasOtherProcessObservers) {
+ snapshot->MarkDirty();
+ }
+ }
+}
+
+void Datastore::Stringify(nsACString& aResult) const {
+ AssertIsOnBackgroundThread();
+
+ aResult.AppendLiteral("DirectoryLock:");
+ aResult.AppendInt(!!mDirectoryLock);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Connection:");
+ aResult.AppendInt(!!mConnection);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("QuotaObject:");
+ aResult.AppendInt(!!mQuotaObject);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PrepareDatastoreOps:");
+ aResult.AppendInt(mPrepareDatastoreOps.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PreparedDatastores:");
+ aResult.AppendInt(mPreparedDatastores.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Databases:");
+ aResult.AppendInt(mDatabases.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("ActiveDatabases:");
+ aResult.AppendInt(mActiveDatabases.Count());
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Origin:");
+ aResult.Append(AnonymizedOriginString(mOriginMetadata.mOrigin));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PrivateBrowsingId:");
+ aResult.AppendInt(mPrivateBrowsingId);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Closed:");
+ aResult.AppendInt(mClosed);
+}
+
+bool Datastore::UpdateUsage(int64_t aDelta) {
+ AssertIsOnBackgroundThread();
+
+ // Check internal LocalStorage origin limit.
+ int64_t newUsage = mUsage + aDelta;
+
+ MOZ_ASSERT(newUsage >= 0);
+
+ if (newUsage > StaticPrefs::dom_storage_default_quota() * 1024) {
+ return false;
+ }
+
+ // Check QuotaManager limits (group and global limit).
+ if (IsPersistent()) {
+ MOZ_ASSERT(mQuotaObject);
+
+ if (!mQuotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+ return false;
+ }
+ }
+
+ // Quota checks passed, set new usage.
+ mUsage = newUsage;
+
+ return true;
+}
+
+void Datastore::MaybeClose() {
+ AssertIsOnBackgroundThread();
+
+ if (!mPrepareDatastoreOps.Count() && !mHasLivePrivateDatastore &&
+ !mPreparedDatastores.Count() && !mDatabases.Count()) {
+ Close();
+ }
+}
+
+void Datastore::ConnectionClosedCallback() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mQuotaObject);
+ MOZ_ASSERT(mClosed);
+
+ // Release the quota object first.
+ mQuotaObject = nullptr;
+
+ bool databaseWasNotAvailable;
+ bool hasCreatedDatabase;
+ mConnection->GetFinishInfo(databaseWasNotAvailable, hasCreatedDatabase);
+
+ if (databaseWasNotAvailable && !hasCreatedDatabase) {
+ MOZ_ASSERT(mUsage == 0);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->ResetUsageForClient(
+ ClientMetadata{mOriginMetadata, mozilla::dom::quota::Client::LS});
+ }
+
+ mConnection = nullptr;
+
+ // Now it's safe to release the directory lock and unregister itself from
+ // the hashtable.
+
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+
+ if (mCompleteCallback) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mCompleteCallback.forget()));
+ }
+}
+
+void Datastore::CleanupMetadata() {
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(gDatastores);
+ const DebugOnly<bool> removed = gDatastores->Remove(mOriginMetadata.mOrigin);
+ MOZ_ASSERT(removed);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS,
+ "Datastore removed"_ns);
+
+ if (!gDatastores->Count()) {
+ gDatastores = nullptr;
+ }
+}
+
+void Datastore::NotifySnapshots(Database* aDatabase, const nsAString& aKey,
+ const LSValue& aOldValue, bool aAffectsOrder) {
+ AssertIsOnBackgroundThread();
+
+ for (Database* database : mDatabases) {
+ MOZ_ASSERT(database);
+
+ if (database == aDatabase) {
+ continue;
+ }
+
+ Snapshot* snapshot = database->GetSnapshot();
+ if (snapshot) {
+ snapshot->SaveItem(aKey, aOldValue, aAffectsOrder);
+ }
+ }
+}
+
+void Datastore::NoteChangedDatabaseMap() {
+ AssertIsOnBackgroundThread();
+
+ for (Database* database : mActiveDatabases) {
+ Snapshot* snapshot = database->GetSnapshot();
+ MOZ_ASSERT(snapshot);
+
+ if (snapshot->IsDirty()) {
+ continue;
+ }
+
+ if (snapshot->HasOtherProcessDatabases() !=
+ HasOtherProcessDatabases(database)) {
+ snapshot->MarkDirty();
+ }
+ }
+}
+
+/*******************************************************************************
+ * PreparedDatastore
+ ******************************************************************************/
+
+void PreparedDatastore::Destroy() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(gPreparedDatastores);
+ DebugOnly<bool> removed = gPreparedDatastores->Remove(mDatastoreId);
+ MOZ_ASSERT(removed);
+}
+
+// static
+void PreparedDatastore::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ AssertIsOnBackgroundThread();
+
+ auto* self = static_cast<PreparedDatastore*>(aClosure);
+ MOZ_ASSERT(self);
+
+ self->Destroy();
+}
+
+/*******************************************************************************
+ * Database
+ ******************************************************************************/
+
+Database::Database(const PrincipalInfo& aPrincipalInfo,
+ const Maybe<ContentParentId>& aContentParentId,
+ const nsACString& aOrigin, uint32_t aPrivateBrowsingId)
+ : mSnapshot(nullptr),
+ mPrincipalInfo(aPrincipalInfo),
+ mContentParentId(aContentParentId),
+ mOrigin(aOrigin),
+ mPrivateBrowsingId(aPrivateBrowsingId),
+ mAllowedToClose(false),
+ mActorDestroyed(false),
+ mRequestedAllowToClose(false)
+#ifdef DEBUG
+ ,
+ mActorWasAlive(false)
+#endif
+{
+ AssertIsOnBackgroundThread();
+}
+
+Database::~Database() {
+ MOZ_ASSERT_IF(mActorWasAlive, mAllowedToClose);
+ MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed);
+}
+
+void Database::SetActorAlive(Datastore* aDatastore) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorWasAlive);
+ MOZ_ASSERT(!mActorDestroyed);
+
+#ifdef DEBUG
+ mActorWasAlive = true;
+#endif
+
+ mDatastore = aDatastore;
+
+ mDatastore->NoteLiveDatabase(this);
+
+ if (!gLiveDatabases) {
+ gLiveDatabases = new LiveDatabaseArray();
+ }
+
+ gLiveDatabases->AppendElement(WrapNotNullUnchecked(this));
+}
+
+void Database::RegisterSnapshot(Snapshot* aSnapshot) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aSnapshot);
+ MOZ_ASSERT(!mSnapshot);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ // Only one snapshot at a time is currently supported.
+ mSnapshot = aSnapshot;
+
+ mDatastore->NoteActiveDatabase(this);
+}
+
+void Database::UnregisterSnapshot(Snapshot* aSnapshot) {
+ MOZ_ASSERT(aSnapshot);
+ MOZ_ASSERT(mSnapshot == aSnapshot);
+
+ mSnapshot = nullptr;
+
+ mDatastore->NoteInactiveDatabase(this);
+}
+
+void Database::RequestAllowToClose() {
+ AssertIsOnBackgroundThread();
+
+ if (mRequestedAllowToClose) {
+ return;
+ }
+
+ mRequestedAllowToClose = true;
+
+ // Send the RequestAllowToClose message to the child to avoid racing with the
+ // child actor. Except the case when the actor was already destroyed.
+ if (mActorDestroyed) {
+ MOZ_ASSERT(mAllowedToClose);
+ return;
+ }
+
+ if (NS_WARN_IF(!SendRequestAllowToClose()) && !mSnapshot) {
+ // This is not necessary, because there should be a runnable scheduled that
+ // will call ActorDestroy which calls AllowToClose. However we can speedup
+ // the shutdown a bit if we do it here directly, but only if there's no
+ // registered snapshot.
+ AllowToClose();
+ }
+}
+
+void Database::ForceKill() {
+ AssertIsOnBackgroundThread();
+
+ if (mActorDestroyed) {
+ MOZ_ASSERT(mAllowedToClose);
+ return;
+ }
+
+ Unused << PBackgroundLSDatabaseParent::Send__delete__(this);
+}
+
+void Database::Stringify(nsACString& aResult) const {
+ AssertIsOnBackgroundThread();
+
+ aResult.AppendLiteral("SnapshotRegistered:");
+ aResult.AppendInt(!!mSnapshot);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("OtherProcessActor:");
+ aResult.AppendInt(BackgroundParent::IsOtherProcessActor(Manager()));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Origin:");
+ aResult.Append(AnonymizedOriginString(mOrigin));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("PrivateBrowsingId:");
+ aResult.AppendInt(mPrivateBrowsingId);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("AllowedToClose:");
+ aResult.AppendInt(mAllowedToClose);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("ActorDestroyed:");
+ aResult.AppendInt(mActorDestroyed);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("RequestedAllowToClose:");
+ aResult.AppendInt(mRequestedAllowToClose);
+}
+
+void Database::AllowToClose() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mAllowedToClose);
+ MOZ_ASSERT(mDatastore);
+ MOZ_ASSERT(!mSnapshot);
+
+ mAllowedToClose = true;
+
+ mDatastore->NoteFinishedDatabase(this);
+
+ mDatastore = nullptr;
+
+ MOZ_ASSERT(gLiveDatabases);
+ gLiveDatabases->RemoveElement(this);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(quota::Client::LS,
+ "Live database removed"_ns);
+
+ if (gLiveDatabases->IsEmpty()) {
+ gLiveDatabases = nullptr;
+ }
+}
+
+void Database::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ mActorDestroyed = true;
+
+ if (!mAllowedToClose) {
+ AllowToClose();
+ }
+}
+
+mozilla::ipc::IPCResult Database::RecvDeleteMe() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSDatabaseParent::Send__delete__(this)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Database::RecvAllowToClose() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mAllowedToClose)) {
+ return IPC_FAIL(this, "mAllowedToClose already set!");
+ }
+
+ AllowToClose();
+
+ return IPC_OK();
+}
+
+PBackgroundLSSnapshotParent* Database::AllocPBackgroundLSSnapshotParent(
+ const nsAString& aDocumentURI, const nsAString& aKey,
+ const bool& aIncreasePeakUsage, const int64_t& aMinSize,
+ LSSnapshotInitInfo* aInitInfo) {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(aIncreasePeakUsage && aMinSize < 0)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(mAllowedToClose)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return nullptr;
+ }
+
+ RefPtr<Snapshot> snapshot = new Snapshot(this, aDocumentURI);
+
+ // Transfer ownership to IPDL.
+ return snapshot.forget().take();
+}
+
+mozilla::ipc::IPCResult Database::RecvPBackgroundLSSnapshotConstructor(
+ PBackgroundLSSnapshotParent* aActor, const nsAString& aDocumentURI,
+ const nsAString& aKey, const bool& aIncreasePeakUsage,
+ const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT_IF(aIncreasePeakUsage, aMinSize >= 0);
+ MOZ_ASSERT(aInitInfo);
+ MOZ_ASSERT(!mAllowedToClose);
+
+ auto* snapshot = static_cast<Snapshot*>(aActor);
+
+ bool addKeyToUnknownItems;
+ nsTHashtable<nsStringHashKey> loadedItems;
+ nsTArray<LSItemInfo> itemInfos;
+ uint32_t nextLoadIndex;
+ LSSnapshot::LoadState loadState;
+ mDatastore->GetSnapshotLoadInfo(aKey, addKeyToUnknownItems, loadedItems,
+ itemInfos, nextLoadIndex, loadState);
+
+ nsTHashSet<nsString> unknownItems;
+ if (addKeyToUnknownItems) {
+ unknownItems.Insert(aKey);
+ }
+
+ uint32_t totalLength = mDatastore->GetLength();
+
+ int64_t usage = mDatastore->GetUsage();
+
+ int64_t peakUsage = usage;
+
+ if (aIncreasePeakUsage) {
+ int64_t size =
+ mDatastore->AttemptToUpdateUsage(aMinSize, /* aInitial */ true);
+
+ peakUsage += size;
+ }
+
+ bool hasOtherProcessDatabases = mDatastore->HasOtherProcessDatabases(this);
+ bool hasOtherProcessObservers = mDatastore->HasOtherProcessObservers(this);
+
+ snapshot->Init(loadedItems, std::move(unknownItems), nextLoadIndex,
+ totalLength, usage, peakUsage, loadState,
+ hasOtherProcessDatabases, hasOtherProcessObservers);
+
+ RegisterSnapshot(snapshot);
+
+ aInitInfo->addKeyToUnknownItems() = addKeyToUnknownItems;
+ aInitInfo->itemInfos() = std::move(itemInfos);
+ aInitInfo->totalLength() = totalLength;
+ aInitInfo->usage() = usage;
+ aInitInfo->peakUsage() = peakUsage;
+ aInitInfo->loadState() = loadState;
+ aInitInfo->hasOtherProcessDatabases() = hasOtherProcessDatabases;
+ aInitInfo->hasOtherProcessObservers() = hasOtherProcessObservers;
+
+ return IPC_OK();
+}
+
+bool Database::DeallocPBackgroundLSSnapshotParent(
+ PBackgroundLSSnapshotParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<Snapshot> actor = dont_AddRef(static_cast<Snapshot*>(aActor));
+
+ return true;
+}
+
+/*******************************************************************************
+ * Snapshot
+ ******************************************************************************/
+
+Snapshot::Snapshot(Database* aDatabase, const nsAString& aDocumentURI)
+ : mDatabase(aDatabase),
+ mDatastore(aDatabase->GetDatastore()),
+ mDocumentURI(aDocumentURI),
+ mTotalLength(0),
+ mUsage(-1),
+ mPeakUsage(-1),
+ mSavedKeys(false),
+ mActorDestroyed(false),
+ mFinishReceived(false),
+ mLoadedReceived(false),
+ mLoadedAllItems(false),
+ mLoadKeysReceived(false),
+ mSentMarkDirty(false) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+}
+
+Snapshot::~Snapshot() {
+ MOZ_ASSERT(mActorDestroyed);
+ MOZ_ASSERT(mFinishReceived);
+}
+
+void Snapshot::SaveItem(const nsAString& aKey, const LSValue& aOldValue,
+ bool aAffectsOrder) {
+ AssertIsOnBackgroundThread();
+
+ MarkDirty();
+
+ if (mLoadedAllItems) {
+ return;
+ }
+
+ if (!mLoadedItems.Contains(aKey) && !mUnknownItems.Contains(aKey)) {
+ mValues.LookupOrInsert(aKey, aOldValue);
+ }
+
+ if (aAffectsOrder && !mSavedKeys) {
+ mDatastore->GetKeys(mKeys);
+ mSavedKeys = true;
+ }
+}
+
+void Snapshot::MarkDirty() {
+ AssertIsOnBackgroundThread();
+
+ if (!mSentMarkDirty) {
+ Unused << SendMarkDirty();
+ mSentMarkDirty = true;
+ }
+}
+
+void Snapshot::Finish() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(mDatastore);
+ MOZ_ASSERT(!mFinishReceived);
+
+ mDatastore->BeginUpdateBatch(mUsage);
+
+ mDatastore->EndUpdateBatch(mPeakUsage);
+
+ mDatabase->UnregisterSnapshot(this);
+
+ mFinishReceived = true;
+}
+
+void Snapshot::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ mActorDestroyed = true;
+
+ if (!mFinishReceived) {
+ Finish();
+ }
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvDeleteMe() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSSnapshotParent::Send__delete__(this)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::Checkpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ AssertIsOnBackgroundThread();
+ // Don't assert `mUsage >= 0`, it can be negative when multiple snapshots are
+ // operating in parallel.
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+
+ if (NS_WARN_IF(aWriteInfos.IsEmpty())) {
+ return IPC_FAIL(this, "aWriteInfos is empty!");
+ }
+
+ if (NS_WARN_IF(mHasOtherProcessObservers)) {
+ return IPC_FAIL(this, "mHasOtherProcessObservers already set!");
+ }
+
+ mDatastore->BeginUpdateBatch(mUsage);
+
+ for (uint32_t index = 0; index < aWriteInfos.Length(); index++) {
+ const LSWriteInfo& writeInfo = aWriteInfos[index];
+
+ switch (writeInfo.type()) {
+ case LSWriteInfo::TLSSetItemInfo: {
+ const LSSetItemInfo& info = writeInfo.get_LSSetItemInfo();
+
+ mDatastore->SetItem(mDatabase, info.key(), info.value());
+
+ break;
+ }
+
+ case LSWriteInfo::TLSRemoveItemInfo: {
+ const LSRemoveItemInfo& info = writeInfo.get_LSRemoveItemInfo();
+
+ mDatastore->RemoveItem(mDatabase, info.key());
+
+ break;
+ }
+
+ case LSWriteInfo::TLSClearInfo: {
+ mDatastore->Clear(mDatabase);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+
+ mUsage = mDatastore->EndUpdateBatch(-1);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::CheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ AssertIsOnBackgroundThread();
+ // Don't assert `mUsage >= 0`, it can be negative when multiple snapshots are
+ // operating in parallel.
+ MOZ_ASSERT(mPeakUsage >= mUsage);
+
+ if (NS_WARN_IF(aWriteAndNotifyInfos.IsEmpty())) {
+ return IPC_FAIL(this, "aWriteAndNotifyInfos is empty!");
+ }
+
+ if (NS_WARN_IF(!mHasOtherProcessObservers)) {
+ return IPC_FAIL(this, "mHasOtherProcessObservers is not set!");
+ }
+
+ mDatastore->BeginUpdateBatch(mUsage);
+
+ for (uint32_t index = 0; index < aWriteAndNotifyInfos.Length(); index++) {
+ const LSWriteAndNotifyInfo& writeAndNotifyInfo =
+ aWriteAndNotifyInfos[index];
+
+ switch (writeAndNotifyInfo.type()) {
+ case LSWriteAndNotifyInfo::TLSSetItemAndNotifyInfo: {
+ const LSSetItemAndNotifyInfo& info =
+ writeAndNotifyInfo.get_LSSetItemAndNotifyInfo();
+
+ mDatastore->SetItem(mDatabase, info.key(), info.value());
+
+ mDatastore->NotifyOtherProcessObservers(
+ mDatabase, mDocumentURI, info.key(), info.oldValue(), info.value());
+
+ break;
+ }
+
+ case LSWriteAndNotifyInfo::TLSRemoveItemAndNotifyInfo: {
+ const LSRemoveItemAndNotifyInfo& info =
+ writeAndNotifyInfo.get_LSRemoveItemAndNotifyInfo();
+
+ mDatastore->RemoveItem(mDatabase, info.key());
+
+ mDatastore->NotifyOtherProcessObservers(mDatabase, mDocumentURI,
+ info.key(), info.oldValue(),
+ VoidLSValue());
+
+ break;
+ }
+
+ case LSWriteAndNotifyInfo::TLSClearInfo: {
+ mDatastore->Clear(mDatabase);
+
+ mDatastore->NotifyOtherProcessObservers(mDatabase, mDocumentURI,
+ VoidString(), VoidLSValue(),
+ VoidLSValue());
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+
+ mUsage = mDatastore->EndUpdateBatch(-1);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvAsyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ return Checkpoint(std::move(aWriteInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvAsyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ return CheckpointAndNotify(std::move(aWriteAndNotifyInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvSyncCheckpoint(
+ nsTArray<LSWriteInfo>&& aWriteInfos) {
+ return Checkpoint(std::move(aWriteInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvSyncCheckpointAndNotify(
+ nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) {
+ return CheckpointAndNotify(std::move(aWriteAndNotifyInfos));
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvAsyncFinish() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return IPC_FAIL(this, "Already finished");
+ }
+
+ Finish();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvSyncFinish() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ MOZ_ASSERT_UNLESS_FUZZING(false);
+ return IPC_FAIL(this, "Already finished");
+ }
+
+ Finish();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoaded() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ return IPC_FAIL(this, "mLoadedReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedAllItems)) {
+ return IPC_FAIL(this, "mLoadedAllItems already set!");
+ }
+
+ if (NS_WARN_IF(mLoadKeysReceived)) {
+ return IPC_FAIL(this, "mLoadKeysReceived already set!");
+ }
+
+ mLoadedReceived = true;
+
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+ mValues.Clear();
+ mKeys.Clear();
+ mLoadedAllItems = true;
+ mLoadKeysReceived = true;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoadValueAndMoreItems(
+ const nsAString& aKey, LSValue* aValue, nsTArray<LSItemInfo>* aItemInfos) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aValue);
+ MOZ_ASSERT(aItemInfos);
+ MOZ_ASSERT(mDatastore);
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ return IPC_FAIL(this, "mLoadedReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedAllItems)) {
+ return IPC_FAIL(this, "mLoadedAllItems already set!");
+ }
+
+ if (mLoadedItems.Contains(aKey)) {
+ return IPC_FAIL(this, "mLoadedItems already contains aKey!");
+ }
+
+ if (mUnknownItems.Contains(aKey)) {
+ return IPC_FAIL(this, "mUnknownItems already contains aKey!");
+ }
+
+ if (auto entry = mValues.Lookup(aKey)) {
+ *aValue = entry.Data();
+ entry.Remove();
+ } else {
+ mDatastore->GetItem(aKey, *aValue);
+ }
+
+ if (aValue->IsVoid()) {
+ mUnknownItems.Insert(aKey);
+ } else {
+ mLoadedItems.PutEntry(aKey);
+
+ // mLoadedItems.Count()==mTotalLength is checked below.
+ }
+
+ // Load some more key/value pairs (as many as the snapshot gradual prefill
+ // byte budget allows).
+
+ if (gSnapshotGradualPrefill > 0) {
+ const nsTArray<LSItemInfo>& orderedItems = mDatastore->GetOrderedItems();
+
+ uint32_t length;
+ if (mSavedKeys) {
+ length = mKeys.Length();
+ } else {
+ length = orderedItems.Length();
+ }
+
+ int64_t size = 0;
+ while (mNextLoadIndex < length) {
+ // If the datastore's ordering has changed, mSavedKeys will be true and
+ // mKeys contains an ordered list of the keys. Otherwise we can use the
+ // datastore's key ordering which is still the same as when the snapshot
+ // was created.
+
+ nsString key;
+ if (mSavedKeys) {
+ key = mKeys[mNextLoadIndex];
+ } else {
+ key = orderedItems[mNextLoadIndex].key();
+ }
+
+ // Normally we would do this:
+ // if (!mLoadedItems.GetEntry(key)) {
+ // ...
+ // mLoadedItems.PutEntry(key);
+ // }
+ // but that requires two hash lookups. We can reduce that to just one
+ // hash lookup if we always call PutEntry and check the number of entries
+ // before and after the put (which is very cheap). However, if we reach
+ // the prefill limit, we need to call RemoveEntry, but that is also cheap
+ // because we pass the entry (not the key).
+
+ uint32_t countBeforePut = mLoadedItems.Count();
+ auto loadedItemEntry = mLoadedItems.PutEntry(key);
+ if (countBeforePut != mLoadedItems.Count()) {
+ // Check mValues first since that contains values as they existed when
+ // our snapshot was created, but have since been changed/removed in the
+ // datastore. If it's not there, then the datastore has the
+ // still-current value. However, if the datastore's key ordering has
+ // changed, we need to do a hash lookup rather than being able to do an
+ // optimized direct access to the index.
+
+ LSValue value;
+ auto valueEntry = mValues.Lookup(key);
+ if (valueEntry) {
+ value = valueEntry.Data();
+ } else if (mSavedKeys) {
+ mDatastore->GetItem(nsString(key), value);
+ } else {
+ value = orderedItems[mNextLoadIndex].value();
+ }
+
+ // All not loaded keys must have a value.
+ MOZ_ASSERT(!value.IsVoid());
+
+ size += static_cast<int64_t>(key.Length()) +
+ static_cast<int64_t>(value.Length());
+
+ if (size > gSnapshotGradualPrefill) {
+ mLoadedItems.RemoveEntry(loadedItemEntry);
+
+ // mNextLoadIndex is not incremented, so we will resume at the same
+ // position next time.
+ break;
+ }
+
+ if (valueEntry) {
+ valueEntry.Remove();
+ }
+
+ LSItemInfo* itemInfo = aItemInfos->AppendElement();
+ itemInfo->key() = key;
+ itemInfo->value() = value;
+ }
+
+ mNextLoadIndex++;
+ }
+ }
+
+ if (mLoadedItems.Count() == mTotalLength) {
+ mLoadedItems.Clear();
+ mUnknownItems.Clear();
+#ifdef DEBUG
+ const bool allValuesVoid =
+ std::all_of(mValues.Values().cbegin(), mValues.Values().cend(),
+ [](const auto& entry) { return entry.IsVoid(); });
+ MOZ_ASSERT(allValuesVoid);
+#endif
+ mValues.Clear();
+ mLoadedAllItems = true;
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvLoadKeys(nsTArray<nsString>* aKeys) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aKeys);
+ MOZ_ASSERT(mDatastore);
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadedReceived)) {
+ return IPC_FAIL(this, "mLoadedReceived already set!");
+ }
+
+ if (NS_WARN_IF(mLoadKeysReceived)) {
+ return IPC_FAIL(this, "mLoadKeysReceived already set!");
+ }
+
+ mLoadKeysReceived = true;
+
+ if (mSavedKeys) {
+ aKeys->AppendElements(std::move(mKeys));
+ } else {
+ mDatastore->GetKeys(*aKeys);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Snapshot::RecvIncreasePeakUsage(const int64_t& aMinSize,
+ int64_t* aSize) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aSize);
+
+ if (NS_WARN_IF(aMinSize <= 0)) {
+ return IPC_FAIL(this, "aMinSize not valid!");
+ }
+
+ if (NS_WARN_IF(mFinishReceived)) {
+ return IPC_FAIL(this, "mFinishReceived already set!");
+ }
+
+ int64_t size =
+ mDatastore->AttemptToUpdateUsage(aMinSize, /* aInitial */ false);
+
+ mPeakUsage += size;
+
+ *aSize = size;
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * Observer
+ ******************************************************************************/
+
+Observer::Observer(const nsACString& aOrigin)
+ : mOrigin(aOrigin), mActorDestroyed(false) {
+ AssertIsOnBackgroundThread();
+}
+
+Observer::~Observer() { MOZ_ASSERT(mActorDestroyed); }
+
+void Observer::Observe(Database* aDatabase, const nsString& aDocumentURI,
+ const nsString& aKey, const LSValue& aOldValue,
+ const LSValue& aNewValue) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aDatabase);
+
+ Unused << SendObserve(aDatabase->GetPrincipalInfo(),
+ aDatabase->PrivateBrowsingId(), aDocumentURI, aKey,
+ aOldValue, aNewValue);
+}
+
+void Observer::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ mActorDestroyed = true;
+
+ MOZ_ASSERT(gObservers);
+
+ nsTArray<NotNull<Observer*>>* array;
+ gObservers->Get(mOrigin, &array);
+ MOZ_ASSERT(array);
+
+ array->RemoveElement(this);
+
+ if (RefPtr<Datastore> datastore = GetDatastore(mOrigin)) {
+ datastore->NoteChangedObserverArray(*array);
+ }
+
+ if (array->IsEmpty()) {
+ gObservers->Remove(mOrigin);
+ }
+
+ if (!gObservers->Count()) {
+ gObservers = nullptr;
+ }
+}
+
+mozilla::ipc::IPCResult Observer::RecvDeleteMe() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mActorDestroyed);
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSObserverParent::Send__delete__(this)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * LSRequestBase
+ ******************************************************************************/
+
+LSRequestBase::LSRequestBase(const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : mParams(aParams),
+ mContentParentId(aContentParentId),
+ mState(State::Initial),
+ mWaitingForFinish(false) {}
+
+LSRequestBase::~LSRequestBase() {
+ MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+ mState == State::Initial || mState == State::Completed);
+}
+
+void LSRequestBase::Dispatch() {
+ AssertIsOnOwningThread();
+
+ mState = State::StartingRequest;
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this));
+}
+
+void LSRequestBase::StringifyState(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ switch (mState) {
+ case State::Initial:
+ aResult.AppendLiteral("Initial");
+ return;
+
+ case State::StartingRequest:
+ aResult.AppendLiteral("StartingRequest");
+ return;
+
+ case State::Nesting:
+ aResult.AppendLiteral("Nesting");
+ return;
+
+ case State::SendingReadyMessage:
+ aResult.AppendLiteral("SendingReadyMessage");
+ return;
+
+ case State::WaitingForFinish:
+ aResult.AppendLiteral("WaitingForFinish");
+ return;
+
+ case State::SendingResults:
+ aResult.AppendLiteral("SendingResults");
+ return;
+
+ case State::Completed:
+ aResult.AppendLiteral("Completed");
+ return;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+}
+
+void LSRequestBase::Stringify(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ aResult.AppendLiteral("State:");
+ StringifyState(aResult);
+}
+
+void LSRequestBase::Log() {
+ AssertIsOnOwningThread();
+
+ if (!LS_LOG_TEST()) {
+ return;
+ }
+
+ LS_LOG(("LSRequestBase [%p]", this));
+
+ nsCString state;
+ StringifyState(state);
+
+ LS_LOG((" mState: %s", state.get()));
+}
+
+nsresult LSRequestBase::NestedRun() { return NS_OK; }
+
+bool LSRequestBase::VerifyRequestParams() {
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(mParams.type() != LSRequestParams::T__None);
+
+ switch (mParams.type()) {
+ case LSRequestParams::TLSRequestPreloadDatastoreParams: {
+ const LSRequestCommonParams& params =
+ mParams.get_LSRequestPreloadDatastoreParams().commonParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(
+ !VerifyOriginKey(params.originKey(), params.principalInfo()))) {
+ return false;
+ }
+
+ break;
+ }
+
+ case LSRequestParams::TLSRequestPrepareDatastoreParams: {
+ const LSRequestPrepareDatastoreParams& params =
+ mParams.get_LSRequestPrepareDatastoreParams();
+
+ const LSRequestCommonParams& commonParams = params.commonParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(commonParams.principalInfo(),
+ commonParams.storagePrincipalInfo(),
+ false))) {
+ return false;
+ }
+
+ if (params.clientPrincipalInfo() &&
+ NS_WARN_IF(!VerifyPrincipalInfo(commonParams.principalInfo(),
+ params.clientPrincipalInfo().ref(),
+ true))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyClientId(mContentParentId,
+ params.clientPrincipalInfo(),
+ params.clientId()))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyOriginKey(commonParams.originKey(),
+ commonParams.principalInfo()))) {
+ return false;
+ }
+
+ break;
+ }
+
+ case LSRequestParams::TLSRequestPrepareObserverParams: {
+ const LSRequestPrepareObserverParams& params =
+ mParams.get_LSRequestPrepareObserverParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ if (params.clientPrincipalInfo() &&
+ NS_WARN_IF(!VerifyPrincipalInfo(params.principalInfo(),
+ params.clientPrincipalInfo().ref(),
+ true))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!VerifyClientId(mContentParentId,
+ params.clientPrincipalInfo(),
+ params.clientId()))) {
+ return false;
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ return true;
+}
+
+nsresult LSRequestBase::StartRequest() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+#ifdef DEBUG
+ // Always verify parameters in DEBUG builds!
+ bool trustParams = false;
+#else
+ bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager());
+#endif
+
+ if (!trustParams && NS_WARN_IF(!VerifyRequestParams())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(Start()));
+
+ return NS_OK;
+}
+
+void LSRequestBase::SendReadyMessage() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ nsresult rv = SendReadyMessageInternal();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MaybeSetFailureCode(rv);
+
+ FinishInternal();
+ }
+}
+
+nsresult LSRequestBase::SendReadyMessageInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage);
+
+ if (!MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (NS_WARN_IF(!SendReady())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mState = State::WaitingForFinish;
+
+ mWaitingForFinish = true;
+
+ return NS_OK;
+}
+
+void LSRequestBase::Finish() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::WaitingForFinish);
+
+ mWaitingForFinish = false;
+
+ FinishInternal();
+}
+
+void LSRequestBase::FinishInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingReadyMessage ||
+ mState == State::WaitingForFinish);
+
+ mState = State::SendingResults;
+
+ // This LSRequestBase 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<LSRequestBase> kungFuDeathGrip = this;
+
+ MOZ_ALWAYS_SUCCEEDS(this->Run());
+}
+
+void LSRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ if (MayProceed()) {
+ LSRequestResponse response;
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ GetResponse(response);
+
+ MOZ_ASSERT(response.type() != LSRequestResponse::T__None);
+
+ if (response.type() == LSRequestResponse::Tnsresult) {
+ MOZ_ASSERT(NS_FAILED(response.get_nsresult()));
+
+ SetFailureCode(response.get_nsresult());
+ }
+ } else {
+ response = ResultCode();
+ }
+
+ Unused << PBackgroundLSRequestParent::Send__delete__(this, response);
+ }
+
+ Cleanup();
+
+ mState = State::Completed;
+}
+
+NS_IMETHODIMP
+LSRequestBase::Run() {
+ nsresult rv;
+
+ switch (mState) {
+ case State::StartingRequest:
+ rv = StartRequest();
+ break;
+
+ case State::Nesting:
+ rv = NestedRun();
+ break;
+
+ case State::SendingReadyMessage:
+ SendReadyMessage();
+ return NS_OK;
+
+ case State::SendingResults:
+ SendResults();
+ return NS_OK;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingReadyMessage) {
+ MaybeSetFailureCode(rv);
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ mState = State::SendingReadyMessage;
+
+ if (IsOnOwningThread()) {
+ SendReadyMessage();
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(
+ OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+ }
+ }
+
+ return NS_OK;
+}
+
+void LSRequestBase::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ NoteComplete();
+
+ // Assume ActorDestroy can happen at any time, so we can't probe the current
+ // state since mState can be modified on any thread (only one thread at a time
+ // based on the state machine). However we can use mWaitingForFinish which is
+ // only touched on the owning thread. If mWaitingForFinisg is true, we can
+ // also modify mState since we are guaranteed that there are no pending
+ // runnables which would probe mState to decide what code needs to run (there
+ // shouldn't be any running runnables on other threads either).
+
+ if (mWaitingForFinish) {
+ Finish();
+ }
+
+ // We don't have to handle the case when mWaitingForFinish 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 LSRequestBase::RecvCancel() {
+ AssertIsOnOwningThread();
+
+ Log();
+
+ const char* crashOnCancel = PR_GetEnv("LSNG_CRASH_ON_CANCEL");
+ if (crashOnCancel) {
+ MOZ_CRASH("LSNG: Crash on cancel.");
+ }
+
+ IProtocol* mgr = Manager();
+ if (!PBackgroundLSRequestParent::Send__delete__(this, NS_ERROR_ABORT)) {
+ return IPC_FAIL(mgr, "Send__delete__ failed!");
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult LSRequestBase::RecvFinish() {
+ AssertIsOnOwningThread();
+
+ Finish();
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * PrepareDatastoreOp
+ ******************************************************************************/
+
+PrepareDatastoreOp::PrepareDatastoreOp(
+ const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSRequestBase(aParams, aContentParentId),
+ mLoadDataOp(nullptr),
+ mPrivateBrowsingId(0),
+ mUsage(0),
+ mSizeOfKeys(0),
+ mSizeOfItems(0),
+ mDatastoreId(0),
+ mNestedState(NestedState::BeforeNesting),
+ mForPreload(aParams.type() ==
+ LSRequestParams::TLSRequestPreloadDatastoreParams),
+ mDatabaseNotAvailable(false),
+ mInvalidated(false)
+#ifdef DEBUG
+ ,
+ mDEBUGUsage(0)
+#endif
+{
+ MOZ_ASSERT(
+ aParams.type() == LSRequestParams::TLSRequestPreloadDatastoreParams ||
+ aParams.type() == LSRequestParams::TLSRequestPrepareDatastoreParams);
+}
+
+PrepareDatastoreOp::~PrepareDatastoreOp() {
+ MOZ_ASSERT(!mDirectoryLock);
+ MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+ mState == State::Initial || mState == State::Completed);
+ MOZ_ASSERT(!mLoadDataOp);
+}
+
+void PrepareDatastoreOp::StringifyNestedState(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ switch (mNestedState) {
+ case NestedState::BeforeNesting:
+ aResult.AppendLiteral("BeforeNesting");
+ return;
+
+ case NestedState::CheckExistingOperations:
+ aResult.AppendLiteral("CheckExistingOperations");
+ return;
+
+ case NestedState::CheckClosingDatastore:
+ aResult.AppendLiteral("CheckClosingDatastore");
+ return;
+
+ case NestedState::PreparationPending:
+ aResult.AppendLiteral("PreparationPending");
+ return;
+
+ case NestedState::DirectoryOpenPending:
+ aResult.AppendLiteral("DirectoryOpenPending");
+ return;
+
+ case NestedState::DatabaseWorkOpen:
+ aResult.AppendLiteral("DatabaseWorkOpen");
+ return;
+
+ case NestedState::BeginLoadData:
+ aResult.AppendLiteral("BeginLoadData");
+ return;
+
+ case NestedState::DatabaseWorkLoadData:
+ aResult.AppendLiteral("DatabaseWorkLoadData");
+ return;
+
+ case NestedState::AfterNesting:
+ aResult.AppendLiteral("AfterNesting");
+ return;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+}
+
+void PrepareDatastoreOp::Stringify(nsACString& aResult) const {
+ AssertIsOnOwningThread();
+
+ LSRequestBase::Stringify(aResult);
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("Origin:");
+ aResult.Append(AnonymizedOriginString(Origin()));
+ aResult.Append(kQuotaGenericDelimiter);
+
+ aResult.AppendLiteral("NestedState:");
+ StringifyNestedState(aResult);
+}
+
+void PrepareDatastoreOp::Log() {
+ AssertIsOnOwningThread();
+
+ LSRequestBase::Log();
+
+ if (!LS_LOG_TEST()) {
+ return;
+ }
+
+ nsCString nestedState;
+ StringifyNestedState(nestedState);
+
+ LS_LOG((" mNestedState: %s", nestedState.get()));
+
+ switch (mNestedState) {
+ case NestedState::CheckClosingDatastore: {
+ for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0;
+ index--) {
+ const auto& existingOp = (*gPrepareDatastoreOps)[index - 1];
+
+ if (existingOp->mDelayedOp == this) {
+ LS_LOG((" mDelayedBy: [%p]",
+ static_cast<PrepareDatastoreOp*>(existingOp.get())));
+
+ existingOp->Log();
+
+ break;
+ }
+ }
+
+ break;
+ }
+
+ case NestedState::DirectoryOpenPending: {
+ MOZ_ASSERT(mPendingDirectoryLock);
+
+ LS_LOG((" mPendingDirectoryLock: [%p]", mPendingDirectoryLock.get()));
+
+ mPendingDirectoryLock->Log();
+
+ break;
+ }
+
+ default:;
+ }
+}
+
+nsresult PrepareDatastoreOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(mNestedState == NestedState::BeforeNesting);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ QM_TRY(QuotaManager::EnsureCreated());
+
+ const LSRequestCommonParams& commonParams =
+ mForPreload
+ ? mParams.get_LSRequestPreloadDatastoreParams().commonParams()
+ : mParams.get_LSRequestPrepareDatastoreParams().commonParams();
+
+ const PrincipalInfo& storagePrincipalInfo =
+ commonParams.storagePrincipalInfo();
+
+ if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ mOriginMetadata = {QuotaManager::GetInfoForChrome(),
+ PERSISTENCE_TYPE_DEFAULT};
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ QM_TRY_UNWRAP(auto principalMetadata,
+ QuotaManager::Get()->GetInfoFromValidatedPrincipalInfo(
+ storagePrincipalInfo));
+
+ mOriginMetadata.mSuffix = std::move(principalMetadata.mSuffix);
+ mOriginMetadata.mGroup = std::move(principalMetadata.mGroup);
+ // XXX We can probably get rid of mMainThreadOrigin if we change
+ // LSRequestBase::Dispatch to synchronously run LSRequestBase::StartRequest
+ // through LSRequestBase::Run.
+ mMainThreadOrigin = std::move(principalMetadata.mOrigin);
+ mOriginMetadata.mStorageOrigin =
+ std::move(principalMetadata.mStorageOrigin);
+ mOriginMetadata.mIsPrivate = principalMetadata.mIsPrivate;
+ mOriginMetadata.mPersistenceType = principalMetadata.mIsPrivate
+ ? PERSISTENCE_TYPE_PRIVATE
+ : PERSISTENCE_TYPE_DEFAULT;
+ }
+
+ mState = State::Nesting;
+ mNestedState = NestedState::CheckExistingOperations;
+
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::CheckExistingOperations() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::CheckExistingOperations);
+ MOZ_ASSERT(gPrepareDatastoreOps);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ const LSRequestCommonParams& commonParams =
+ mForPreload
+ ? mParams.get_LSRequestPreloadDatastoreParams().commonParams()
+ : mParams.get_LSRequestPrepareDatastoreParams().commonParams();
+
+ const PrincipalInfo& storagePrincipalInfo =
+ commonParams.storagePrincipalInfo();
+
+ nsCString originAttrSuffix;
+ uint32_t privateBrowsingId;
+
+ if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ privateBrowsingId = 0;
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ const ContentPrincipalInfo& info =
+ storagePrincipalInfo.get_ContentPrincipalInfo();
+ const OriginAttributes& attrs = info.attrs();
+ attrs.CreateSuffix(originAttrSuffix);
+
+ privateBrowsingId = attrs.mPrivateBrowsingId;
+ }
+
+ mArchivedOriginScope = ArchivedOriginScope::CreateFromOrigin(
+ originAttrSuffix, commonParams.originKey());
+ MOZ_ASSERT(mArchivedOriginScope);
+
+ // Normally it's safe to access member variables without a mutex because even
+ // though we hop between threads, the variables are never accessed by multiple
+ // threads at the same time.
+ // However, the methods OriginIsKnown and Origin can be called at any time.
+ // So we have to make sure the member variable is set on the same thread as
+ // those methods are called.
+ mOriginMetadata.mOrigin = mMainThreadOrigin;
+
+ MOZ_ASSERT(OriginIsKnown());
+
+ mPrivateBrowsingId = privateBrowsingId;
+
+ mNestedState = NestedState::CheckClosingDatastore;
+
+ // See if this PrepareDatastoreOp needs to wait.
+ bool foundThis = false;
+ for (uint32_t index = gPrepareDatastoreOps->Length(); index > 0; index--) {
+ const auto& existingOp = (*gPrepareDatastoreOps)[index - 1];
+
+ if (existingOp == this) {
+ foundThis = true;
+ continue;
+ }
+
+ if (foundThis && existingOp->Origin() == Origin()) {
+ // Only one op can be delayed.
+ MOZ_ASSERT(!existingOp->mDelayedOp);
+ existingOp->mDelayedOp = this;
+
+ return NS_OK;
+ }
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CheckClosingDatastoreInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::CheckClosingDatastore() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CheckClosingDatastoreInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::CheckClosingDatastoreInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::CheckClosingDatastore);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ mNestedState = NestedState::PreparationPending;
+
+ RefPtr<Datastore> datastore;
+ if ((datastore = GetDatastore(Origin())) && datastore->IsClosed()) {
+ datastore->WaitForConnectionToComplete(this);
+
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(BeginDatastorePreparationInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::BeginDatastorePreparation() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::PreparationPending);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(BeginDatastorePreparationInternal()));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::BeginDatastorePreparationInternal() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::PreparationPending);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+ MOZ_ASSERT(OriginIsKnown());
+ MOZ_ASSERT(!mDirectoryLock);
+
+ if ((mDatastore = GetDatastore(Origin()))) {
+ MOZ_ASSERT(!mDatastore->IsClosed());
+
+ mDatastore->NoteLivePrepareDatastoreOp(this);
+
+ FinishNesting();
+
+ return NS_OK;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ mNestedState = NestedState::DirectoryOpenPending;
+
+ quotaManager
+ ->OpenClientDirectory({mOriginMetadata, mozilla::dom::quota::Client::LS},
+ SomeRef(mPendingDirectoryLock))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this)](
+ const ClientDirectoryLockPromise::ResolveOrRejectValue& aValue) {
+ self->mPendingDirectoryLock = nullptr;
+
+ if (aValue.IsResolve()) {
+ self->DirectoryLockAcquired(aValue.ResolveValue());
+ } else {
+ self->DirectoryLockFailed();
+ }
+ });
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::SendToIOThread() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ // Skip all disk related stuff and transition to SendingReadyMessage if we
+ // are preparing a datastore for private browsing.
+ // Note that we do use a directory lock for private browsing even though we
+ // don't do any stuff on disk. The thing is that without a directory lock,
+ // quota manager wouldn't call AbortOperationsForLocks for our private
+ // browsing origin when a clear origin operation is requested.
+ // AbortOperationsForLocks requests all databases to close and the datastore
+ // is destroyed in the end. Any following LocalStorage API call will trigger
+ // preparation of a new (empty) datastore.
+ if (mPrivateBrowsingId) {
+ FinishNesting();
+
+ return;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // Must set this before dispatching otherwise we will race with the IO thread.
+ mNestedState = NestedState::DatabaseWorkOpen;
+
+ MOZ_ALWAYS_SUCCEEDS(
+ quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+nsresult PrepareDatastoreOp::DatabaseWork() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mArchivedOriginScope);
+ MOZ_ASSERT(mUsage == 0);
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
+
+ const auto innerFunc = [&](const auto&) -> nsresult {
+ // XXX This function is too long, refactor it into helper functions for
+ // readability.
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+ !MayProceedOnNonOwningThread()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ // This ensures that usages for existings origin directories are cached in
+ // memory.
+ QM_TRY(MOZ_TO_RESULT(
+ quotaManager->EnsureTemporaryStorageIsInitializedInternal()));
+
+ const UsageInfo usageInfo = quotaManager->GetUsageForClient(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata,
+ mozilla::dom::quota::Client::LS);
+
+ const bool hasUsage = usageInfo.DatabaseUsage().isSome();
+ MOZ_ASSERT(usageInfo.FileUsage().isNothing());
+
+ if (!gArchivedOrigins) {
+ QM_TRY(MOZ_TO_RESULT(LoadArchivedOrigins()));
+ MOZ_ASSERT(gArchivedOrigins);
+ }
+
+ bool hasDataForMigration =
+ mArchivedOriginScope->HasMatches(gArchivedOrigins);
+
+ // If there's nothing to preload (except the case when we want to migrate
+ // data during preloading), then we can finish the operation without
+ // creating a datastore in GetResponse (GetResponse won't create a datastore
+ // if mDatatabaseNotAvailable and mForPreload are both true).
+ if (mForPreload && !hasUsage && !hasDataForMigration) {
+ return DatabaseNotAvailable();
+ }
+
+ // The origin directory doesn't need to be created when we don't have data
+ // for migration. It will be created on the connection thread in
+ // Connection::EnsureStorageConnection.
+ // However, origin quota must be initialized, GetQuotaObject in GetResponse
+ // would fail otherwise.
+ QM_TRY_INSPECT(
+ const auto& directoryEntry,
+ ([hasDataForMigration, &quotaManager,
+ this]() -> mozilla::Result<nsCOMPtr<nsIFile>, nsresult> {
+ if (hasDataForMigration) {
+ QM_TRY_RETURN(quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata)
+ .map([](const auto& res) { return res.first; }));
+ }
+
+ MOZ_ASSERT(mOriginMetadata.mPersistenceType ==
+ PERSISTENCE_TYPE_DEFAULT);
+
+ QM_TRY_UNWRAP(auto directoryEntry,
+ quotaManager->GetOriginDirectory(mOriginMetadata));
+
+ quotaManager->EnsureQuotaForOrigin(mOriginMetadata);
+
+ return directoryEntry;
+ }()));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(
+ NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+ QM_TRY_INSPECT(
+ const auto& directoryPath,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directoryEntry, GetPath));
+
+ // The ls directory doesn't need to be created when we don't have data for
+ // migration. It will be created on the connection thread in
+ // Connection::EnsureStorageConnection.
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureDirectoryEntry(directoryEntry,
+ /* aCreateIfNotExists */ hasDataForMigration,
+ /* aIsDirectory */ true)));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->Append(kDataFileName)));
+
+ QM_TRY(MOZ_TO_RESULT(directoryEntry->GetPath(mDatabaseFilePath)));
+
+ // The database doesn't need to be created when we don't have data for
+ // migration. It will be created on the connection thread in
+ // Connection::EnsureStorageConnection.
+ bool alreadyExisted;
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureDirectoryEntry(directoryEntry,
+ /* aCreateIfNotExists */ hasDataForMigration,
+ /* aIsDirectory */ false, &alreadyExisted)));
+
+ if (alreadyExisted) {
+ // The database does exist.
+ MOZ_ASSERT(hasUsage);
+
+ // XXX Change type of mUsage to UsageInfo or DatabaseUsageType.
+ mUsage = usageInfo.DatabaseUsage().valueOr(0);
+ } else {
+ // The database doesn't exist.
+ MOZ_ASSERT(!hasUsage);
+
+ if (!hasDataForMigration) {
+ // The database doesn't exist and we don't have data for migration.
+ // Finish the operation, but create an empty datastore in GetResponse
+ // (GetResponse will create an empty datastore if mDatabaseNotAvailable
+ // is true and mForPreload is false).
+ return DatabaseNotAvailable();
+ }
+ }
+
+ // We initialized mDatabaseFilePath and mUsage, GetQuotaObject can now be
+ // called.
+ const RefPtr<QuotaObject> quotaObject = GetQuotaObject();
+
+ QM_TRY(OkIf(quotaObject), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath));
+
+ QM_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(directoryPath));
+
+ QM_TRY_INSPECT(
+ const auto& connection,
+ (CreateStorageConnectionWithRecovery(
+ *directoryEntry, *usageFile, Origin(), [&quotaObject, this] {
+ // This is called when the usage file was removed or we notice
+ // that the usage file doesn't exist anymore. Adjust the usage
+ // accordingly.
+
+ MOZ_ALWAYS_TRUE(
+ quotaObject->MaybeUpdateSize(0, /* aTruncate */ true));
+
+ mUsage = 0;
+ })));
+
+ QM_TRY(MOZ_TO_RESULT(VerifyDatabaseInformation(connection)));
+
+ if (hasDataForMigration) {
+ MOZ_ASSERT(mUsage == 0);
+
+ {
+ QM_TRY_INSPECT(const auto& archiveFile,
+ GetArchiveFile(quotaManager->GetStoragePath()));
+
+ auto autoArchiveDatabaseAttacher =
+ AutoDatabaseAttacher(connection, archiveFile, "archive"_ns);
+
+ QM_TRY(MOZ_TO_RESULT(autoArchiveDatabaseAttacher.Attach()));
+
+ QM_TRY_INSPECT(const int64_t& newUsage,
+ GetUsage(*connection, mArchivedOriginScope.get()));
+
+ QM_TRY(
+ OkIf(quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)),
+ NS_ERROR_FILE_NO_DEVICE_SPACE);
+
+ auto autoUpdateSize = MakeScopeExit([&quotaObject] {
+ MOZ_ALWAYS_TRUE(
+ quotaObject->MaybeUpdateSize(0, /* aTruncate */ true));
+ });
+
+ mozStorageTransaction transaction(
+ connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ {
+ nsCOMPtr<mozIStorageFunction> function = new CompressFunction();
+
+ QM_TRY(MOZ_TO_RESULT(
+ connection->CreateFunction("compress"_ns, 1, function)));
+
+ function = new CompressionTypeFunction();
+
+ QM_TRY(MOZ_TO_RESULT(
+ connection->CreateFunction("compressionType"_ns, 1, function)));
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "INSERT INTO data (key, utf16_length, conversion_type, "
+ "compression_type, value) "
+ "SELECT key, utf16Length(value), :conversionType, "
+ "compressionType(value), compress(value)"
+ "FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName(
+ "conversionType"_ns,
+ static_cast<int32_t>(LSValue::ConversionType::UTF16_UTF8))));
+
+ QM_TRY(MOZ_TO_RESULT(mArchivedOriginScope->BindToStatement(stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ QM_TRY(MOZ_TO_RESULT(connection->RemoveFunction("compress"_ns)));
+
+ QM_TRY(
+ MOZ_TO_RESULT(connection->RemoveFunction("compressionType"_ns)));
+ }
+
+ {
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "UPDATE database SET usage = :usage;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt64ByName("usage"_ns, newUsage)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ {
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection, CreateStatement,
+ "DELETE FROM webappsstore2 "
+ "WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(mArchivedOriginScope->BindToStatement(stmt)));
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ UpdateUsageFile(usageFile, usageJournalFile, newUsage)));
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+
+ autoUpdateSize.release();
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+
+ mUsage = newUsage;
+
+ QM_TRY(MOZ_TO_RESULT(autoArchiveDatabaseAttacher.Detach()));
+ }
+
+ MOZ_ASSERT(gArchivedOrigins);
+ MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins));
+ mArchivedOriginScope->RemoveMatches(gArchivedOrigins);
+ }
+
+ nsCOMPtr<mozIStorageConnection> shadowConnection;
+ if (!gInitializedShadowStorage) {
+ QM_TRY_UNWRAP(shadowConnection,
+ CreateShadowStorageConnection(quotaManager->GetBasePath()));
+
+ gInitializedShadowStorage = true;
+ }
+
+ // Must close connections before dispatching otherwise we might race with
+ // the connection thread which needs to open the same databases.
+ MOZ_ALWAYS_SUCCEEDS(connection->Close());
+
+ if (shadowConnection) {
+ MOZ_ALWAYS_SUCCEEDS(shadowConnection->Close());
+ }
+
+ // Must set this before dispatching otherwise we will race with the owning
+ // thread.
+ mNestedState = NestedState::BeginLoadData;
+
+ QM_TRY(
+ MOZ_TO_RESULT(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)));
+
+ return NS_OK;
+ };
+
+ return ExecuteOriginInitialization(
+ mOriginMetadata.mOrigin, LSOriginInitialization::Datastore,
+ "dom::localstorage::FirstOriginInitializationAttempt::Datastore"_ns,
+ innerFunc);
+}
+
+nsresult PrepareDatastoreOp::DatabaseNotAvailable() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
+
+ mDatabaseNotAvailable = true;
+
+ nsresult rv = FinishNestingOnNonOwningThread();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::EnsureDirectoryEntry(nsIFile* aEntry,
+ bool aCreateIfNotExists,
+ bool aIsDirectory,
+ bool* aAlreadyExisted) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aEntry);
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aEntry, Exists));
+
+ if (!exists) {
+ if (!aCreateIfNotExists) {
+ if (aAlreadyExisted) {
+ *aAlreadyExisted = false;
+ }
+ return NS_OK;
+ }
+
+ if (aIsDirectory) {
+ QM_TRY(MOZ_TO_RESULT(aEntry->Create(nsIFile::DIRECTORY_TYPE, 0755)));
+ }
+ }
+#ifdef DEBUG
+ else {
+ bool isDirectory;
+ MOZ_ASSERT(NS_SUCCEEDED(aEntry->IsDirectory(&isDirectory)));
+ MOZ_ASSERT(isDirectory == aIsDirectory);
+ }
+#endif
+
+ if (aAlreadyExisted) {
+ *aAlreadyExisted = exists;
+ }
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::VerifyDatabaseInformation(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ QM_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ *aConnection, "SELECT origin FROM database"_ns));
+
+ QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED);
+
+ QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, stmt, GetUTF8String, 0));
+
+ QM_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(Origin(), origin)),
+ NS_ERROR_FILE_CORRUPTED);
+
+ return NS_OK;
+}
+
+already_AddRefed<QuotaObject> PrepareDatastoreOp::GetQuotaObject() {
+ MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+ MOZ_ASSERT(!mOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(OriginIsKnown());
+ MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject(
+ PERSISTENCE_TYPE_DEFAULT, mOriginMetadata,
+ mozilla::dom::quota::Client::LS, mDatabaseFilePath, mUsage);
+
+ if (!quotaObject) {
+ LS_WARNING("Failed to get quota object for group (%s) and origin (%s)!",
+ mOriginMetadata.mGroup.get(), Origin().get());
+ }
+
+ return quotaObject.forget();
+}
+
+nsresult PrepareDatastoreOp::BeginLoadData() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::BeginLoadData);
+ MOZ_ASSERT(!mConnection);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (!gConnectionThread) {
+ gConnectionThread = new ConnectionThread();
+ }
+
+ mConnection = gConnectionThread->CreateConnection(
+ mOriginMetadata, std::move(mArchivedOriginScope),
+ /* aDatabaseWasNotAvailable */ false);
+ MOZ_ASSERT(mConnection);
+
+ // Must set this before dispatching otherwise we will race with the
+ // connection thread.
+ mNestedState = NestedState::DatabaseWorkLoadData;
+
+ // Can't assign to mLoadDataOp directly since that's a weak reference and
+ // LoadDataOp is reference counted.
+ RefPtr<LoadDataOp> loadDataOp = new LoadDataOp(this);
+
+ // This add refs loadDataOp.
+ mConnection->Dispatch(loadDataOp);
+
+ // This is cleared in LoadDataOp::Cleanup() before the load data op is
+ // destroyed.
+ mLoadDataOp = loadDataOp;
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::FinishNesting() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+
+ // The caller holds a strong reference to us, no need for a self reference
+ // before calling Run().
+
+ mState = State::SendingReadyMessage;
+ mNestedState = NestedState::AfterNesting;
+
+ MOZ_ALWAYS_SUCCEEDS(Run());
+}
+
+nsresult PrepareDatastoreOp::FinishNestingOnNonOwningThread() {
+ MOZ_ASSERT(!IsOnOwningThread());
+ MOZ_ASSERT(mState == State::Nesting);
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ mState = State::SendingReadyMessage;
+ mNestedState = NestedState::AfterNesting;
+
+ QM_TRY(
+ MOZ_TO_RESULT(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)));
+
+ return NS_OK;
+}
+
+nsresult PrepareDatastoreOp::NestedRun() {
+ nsresult rv;
+
+ switch (mNestedState) {
+ case NestedState::CheckExistingOperations:
+ rv = CheckExistingOperations();
+ break;
+
+ case NestedState::CheckClosingDatastore:
+ rv = CheckClosingDatastore();
+ break;
+
+ case NestedState::PreparationPending:
+ rv = BeginDatastorePreparation();
+ break;
+
+ case NestedState::DatabaseWorkOpen:
+ rv = DatabaseWork();
+ break;
+
+ case NestedState::BeginLoadData:
+ rv = BeginLoadData();
+ break;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mNestedState = NestedState::AfterNesting;
+
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::GetResponse(LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ // A datastore is not created when we are just trying to preload data and
+ // there's no database file.
+ if (mDatabaseNotAvailable && mForPreload) {
+ LSRequestPreloadDatastoreResponse preloadDatastoreResponse;
+
+ aResponse = preloadDatastoreResponse;
+
+ return;
+ }
+
+ if (!mDatastore) {
+ MOZ_ASSERT(mUsage == mDEBUGUsage);
+
+ RefPtr<QuotaObject> quotaObject;
+
+ if (mPrivateBrowsingId == 0) {
+ if (!mConnection) {
+ // This can happen when there's no database file.
+ MOZ_ASSERT(mDatabaseNotAvailable);
+
+ // Even though there's no database file, we need to create a connection
+ // and pass it to datastore.
+ if (!gConnectionThread) {
+ gConnectionThread = new ConnectionThread();
+ }
+
+ mConnection = gConnectionThread->CreateConnection(
+ mOriginMetadata, std::move(mArchivedOriginScope),
+ /* aDatabaseWasNotAvailable */ true);
+ MOZ_ASSERT(mConnection);
+ }
+
+ quotaObject = GetQuotaObject();
+ if (!quotaObject) {
+ aResponse = NS_ERROR_FAILURE;
+ return;
+ }
+ }
+
+ mDatastore = new Datastore(
+ mOriginMetadata, mPrivateBrowsingId, mUsage, mSizeOfKeys, mSizeOfItems,
+ std::move(mDirectoryLock), std::move(mConnection),
+ std::move(quotaObject), mValues, std::move(mOrderedItems));
+
+ mDatastore->NoteLivePrepareDatastoreOp(this);
+
+ if (!gDatastores) {
+ gDatastores = new DatastoreHashtable();
+ }
+
+ MOZ_ASSERT(!gDatastores->Contains(Origin()));
+ gDatastores->InsertOrUpdate(Origin(),
+ WrapMovingNotNullUnchecked(mDatastore));
+ }
+
+ if (mPrivateBrowsingId && !mInvalidated) {
+ if (!gPrivateDatastores) {
+ gPrivateDatastores = MakeUnique<PrivateDatastoreHashtable>();
+ }
+
+ gPrivateDatastores->LookupOrInsertWith(Origin(), [&] {
+ auto privateDatastore =
+ MakeUnique<PrivateDatastore>(WrapMovingNotNull(mDatastore));
+
+ mPrivateDatastoreRegistered.Flip();
+
+ return privateDatastore;
+ });
+ }
+
+ mDatastoreId = ++gLastDatastoreId;
+
+ if (!gPreparedDatastores) {
+ gPreparedDatastores = new PreparedDatastoreHashtable();
+ }
+ const auto& preparedDatastore = gPreparedDatastores->InsertOrUpdate(
+ mDatastoreId, MakeUnique<PreparedDatastore>(
+ mDatastore, mContentParentId, Origin(), mDatastoreId,
+ /* aForPreload */ mForPreload));
+
+ if (mInvalidated) {
+ preparedDatastore->Invalidate();
+ }
+
+ mPreparedDatastoreRegistered.Flip();
+
+ if (mForPreload) {
+ LSRequestPreloadDatastoreResponse preloadDatastoreResponse;
+
+ aResponse = preloadDatastoreResponse;
+ } else {
+ LSRequestPrepareDatastoreResponse prepareDatastoreResponse;
+ prepareDatastoreResponse.datastoreId() = mDatastoreId;
+
+ aResponse = prepareDatastoreResponse;
+ }
+}
+
+void PrepareDatastoreOp::Cleanup() {
+ AssertIsOnOwningThread();
+
+ if (mDatastore) {
+ MOZ_ASSERT(!mDirectoryLock);
+ MOZ_ASSERT(!mConnection);
+
+ if (NS_FAILED(ResultCode())) {
+ if (mPrivateDatastoreRegistered) {
+ MOZ_ASSERT(gPrivateDatastores);
+ DebugOnly<bool> removed = gPrivateDatastores->Remove(Origin());
+ MOZ_ASSERT(removed);
+
+ if (!gPrivateDatastores->Count()) {
+ gPrivateDatastores = nullptr;
+ }
+ }
+
+ if (mPreparedDatastoreRegistered) {
+ // Just in case we failed to send datastoreId to the child, we need to
+ // destroy prepared datastore, otherwise it won't be destroyed until
+ // the timer fires (after 20 seconds).
+ MOZ_ASSERT(gPreparedDatastores);
+ MOZ_ASSERT(mDatastoreId > 0);
+ DebugOnly<bool> removed = gPreparedDatastores->Remove(mDatastoreId);
+ MOZ_ASSERT(removed);
+
+ if (!gPreparedDatastores->Count()) {
+ gPreparedDatastores = nullptr;
+ }
+ }
+ }
+
+ // Make sure to release the datastore on this thread.
+
+ mDatastore->NoteFinishedPrepareDatastoreOp(this);
+
+ mDatastore = nullptr;
+
+ CleanupMetadata();
+ } else if (mConnection) {
+ // If we have a connection then the operation must have failed and there
+ // must be a directory lock too.
+ MOZ_ASSERT(NS_FAILED(ResultCode()));
+ MOZ_ASSERT(mDirectoryLock);
+
+ // We must close the connection on the connection thread before releasing
+ // it on this thread. The directory lock can't be released either.
+ nsCOMPtr<nsIRunnable> callback =
+ NewRunnableMethod("dom::OpenDatabaseOp::ConnectionClosedCallback", this,
+ &PrepareDatastoreOp::ConnectionClosedCallback);
+
+ mConnection->Close(callback);
+ } else {
+ // If we don't have a connection, but we do have a directory lock then the
+ // operation must have failed or we were preloading a datastore and there
+ // was no physical database on disk.
+ MOZ_ASSERT_IF(mDirectoryLock,
+ NS_FAILED(ResultCode()) || mDatabaseNotAvailable);
+
+ // There's no connection, so it's safe to release the directory lock and
+ // unregister itself from the array.
+
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+ }
+}
+
+void PrepareDatastoreOp::ConnectionClosedCallback() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NS_FAILED(ResultCode()));
+ MOZ_ASSERT(mDirectoryLock);
+ MOZ_ASSERT(mConnection);
+
+ mConnection = nullptr;
+ mDirectoryLock = nullptr;
+
+ CleanupMetadata();
+}
+
+void PrepareDatastoreOp::CleanupMetadata() {
+ AssertIsOnOwningThread();
+
+ if (mDelayedOp) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget()));
+ }
+
+ MOZ_ASSERT(gPrepareDatastoreOps);
+ gPrepareDatastoreOps->RemoveElement(this);
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ quota::Client::LS, "PrepareDatastoreOp completed"_ns);
+
+ if (gPrepareDatastoreOps->IsEmpty()) {
+ gPrepareDatastoreOps = nullptr;
+ }
+}
+
+void PrepareDatastoreOp::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ LSRequestBase::ActorDestroy(aWhy);
+
+ if (mLoadDataOp) {
+ mLoadDataOp->NoteComplete();
+ }
+}
+
+void PrepareDatastoreOp::DirectoryLockAcquired(DirectoryLock* aLock) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+ MOZ_ASSERT(!mDirectoryLock);
+
+ mPendingDirectoryLock = nullptr;
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+
+ FinishNesting();
+
+ return;
+ }
+
+ mDirectoryLock = aLock;
+
+ SendToIOThread();
+}
+
+void PrepareDatastoreOp::DirectoryLockFailed() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::Nesting);
+ MOZ_ASSERT(mNestedState == NestedState::DirectoryOpenPending);
+ MOZ_ASSERT(!mDirectoryLock);
+
+ mPendingDirectoryLock = nullptr;
+
+ MaybeSetFailureCode(NS_ERROR_FAILURE);
+
+ FinishNesting();
+}
+
+nsresult PrepareDatastoreOp::LoadDataOp::DoDatastoreWork() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting);
+ MOZ_ASSERT(mPrepareDatastoreOp->mNestedState ==
+ NestedState::DatabaseWorkLoadData);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
+ !MayProceedOnNonOwningThread()) {
+ return NS_ERROR_ABORT;
+ }
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ mConnection->BorrowCachedStatement(
+ "SELECT key, utf16_length, conversion_type, compression_type, value "
+ "FROM data;"_ns));
+
+ QM_TRY(quota::CollectWhileHasResult(
+ *stmt, [this](auto& stmt) -> mozilla::Result<Ok, nsresult> {
+ QM_TRY_UNWRAP(auto key, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, stmt, GetString, 0));
+
+ LSValue value;
+ QM_TRY(MOZ_TO_RESULT(value.InitFromStatement(&stmt, 1)));
+
+ mPrepareDatastoreOp->mValues.InsertOrUpdate(key, value);
+ mPrepareDatastoreOp->mSizeOfKeys += key.Length();
+ mPrepareDatastoreOp->mSizeOfItems += key.Length() + value.Length();
+#ifdef DEBUG
+ mPrepareDatastoreOp->mDEBUGUsage += key.Length() + value.UTF16Length();
+#endif
+
+ auto item = mPrepareDatastoreOp->mOrderedItems.AppendElement();
+ item->key() = std::move(key);
+ item->value() = std::move(value);
+
+ return Ok{};
+ }));
+
+ return NS_OK;
+}
+
+void PrepareDatastoreOp::LoadDataOp::OnSuccess() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting);
+ MOZ_ASSERT(mPrepareDatastoreOp->mNestedState ==
+ NestedState::DatabaseWorkLoadData);
+ MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this);
+
+ mPrepareDatastoreOp->FinishNesting();
+}
+
+void PrepareDatastoreOp::LoadDataOp::OnFailure(nsresult aResultCode) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mState == State::Nesting);
+ MOZ_ASSERT(mPrepareDatastoreOp->mNestedState ==
+ NestedState::DatabaseWorkLoadData);
+ MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this);
+
+ mPrepareDatastoreOp->SetFailureCode(aResultCode);
+
+ mPrepareDatastoreOp->FinishNesting();
+}
+
+void PrepareDatastoreOp::LoadDataOp::Cleanup() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mPrepareDatastoreOp);
+ MOZ_ASSERT(mPrepareDatastoreOp->mLoadDataOp == this);
+
+ mPrepareDatastoreOp->mLoadDataOp = nullptr;
+ mPrepareDatastoreOp = nullptr;
+
+ ConnectionDatastoreOperationBase::Cleanup();
+}
+
+NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+PrepareDatastoreOp::CompressFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+#ifdef DEBUG
+ {
+ uint32_t argCount;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount));
+ MOZ_ASSERT(argCount == 1);
+
+ int32_t type;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetTypeOfIndex(0, &type));
+ MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& value,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, aFunctionArguments, GetUTF8String, 0));
+
+ nsCString compressed;
+ QM_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_OUT_OF_MEMORY);
+
+ const nsCString& buffer = compressed.IsVoid() ? value : compressed;
+
+ // mozStorage transforms empty blobs into null values, but our database
+ // schema doesn't allow null values. We can workaround this by storing
+ // empty buffers as UTF8 text (SQLite supports the type affinity, so the type
+ // of the column is not fixed).
+ nsCOMPtr<nsIVariant> result;
+ if (0u == buffer.Length()) { // Otherwise empty string becomes null
+ result = new storage::UTF8TextVariant(buffer);
+ } else {
+ result = new storage::BlobVariant(std::make_pair(
+ static_cast<const void*>(buffer.get()), int(buffer.Length())));
+ }
+
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressionTypeFunction,
+ mozIStorageFunction)
+
+NS_IMETHODIMP
+PrepareDatastoreOp::CompressionTypeFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+#ifdef DEBUG
+ {
+ uint32_t argCount;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount));
+ MOZ_ASSERT(argCount == 1);
+
+ int32_t type;
+ MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetTypeOfIndex(0, &type));
+ MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& value,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, aFunctionArguments, GetUTF8String, 0));
+
+ nsCString compressed;
+ QM_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_OUT_OF_MEMORY);
+
+ const int32_t compression = static_cast<int32_t>(
+ compressed.IsVoid() ? LSValue::CompressionType::UNCOMPRESSED
+ : LSValue::CompressionType::SNAPPY);
+
+ nsCOMPtr<nsIVariant> result = new storage::IntegerVariant(compression);
+
+ result.forget(aResult);
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * PrepareObserverOp
+ ******************************************************************************/
+
+PrepareObserverOp::PrepareObserverOp(
+ const LSRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSRequestBase(aParams, aContentParentId) {
+ MOZ_ASSERT(aParams.type() ==
+ LSRequestParams::TLSRequestPrepareObserverParams);
+}
+
+nsresult PrepareObserverOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSRequestPrepareObserverParams params =
+ mParams.get_LSRequestPrepareObserverParams();
+
+ const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo();
+
+ if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ mOrigin = QuotaManager::GetOriginForChrome();
+ } else {
+ MOZ_ASSERT(storagePrincipalInfo.type() ==
+ PrincipalInfo::TContentPrincipalInfo);
+
+ mOrigin =
+ QuotaManager::GetOriginFromValidatedPrincipalInfo(storagePrincipalInfo);
+ }
+
+ mState = State::SendingReadyMessage;
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void PrepareObserverOp::GetResponse(LSRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ uint64_t observerId = ++gLastObserverId;
+
+ RefPtr<Observer> observer = new Observer(mOrigin);
+
+ if (!gPreparedObsevers) {
+ gPreparedObsevers = new PreparedObserverHashtable();
+ }
+ gPreparedObsevers->InsertOrUpdate(observerId, std::move(observer));
+
+ LSRequestPrepareObserverResponse prepareObserverResponse;
+ prepareObserverResponse.observerId() = observerId;
+
+ aResponse = prepareObserverResponse;
+}
+
+/*******************************************************************************
++ * LSSimpleRequestBase
++
+******************************************************************************/
+
+LSSimpleRequestBase::LSSimpleRequestBase(
+ const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : mParams(aParams),
+ mContentParentId(aContentParentId),
+ mState(State::Initial) {}
+
+LSSimpleRequestBase::~LSSimpleRequestBase() {
+ MOZ_ASSERT_IF(MayProceedOnNonOwningThread(),
+ mState == State::Initial || mState == State::Completed);
+}
+
+void LSSimpleRequestBase::Dispatch() {
+ AssertIsOnOwningThread();
+
+ mState = State::StartingRequest;
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this));
+}
+
+bool LSSimpleRequestBase::VerifyRequestParams() {
+ AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT(mParams.type() != LSSimpleRequestParams::T__None);
+
+ switch (mParams.type()) {
+ case LSSimpleRequestParams::TLSSimpleRequestPreloadedParams: {
+ const LSSimpleRequestPreloadedParams& params =
+ mParams.get_LSSimpleRequestPreloadedParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ break;
+ }
+
+ case LSSimpleRequestParams::TLSSimpleRequestGetStateParams: {
+ const LSSimpleRequestGetStateParams& params =
+ mParams.get_LSSimpleRequestGetStateParams();
+
+ if (NS_WARN_IF(!VerifyPrincipalInfo(
+ params.principalInfo(), params.storagePrincipalInfo(), false))) {
+ return false;
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ return true;
+}
+
+nsresult LSSimpleRequestBase::StartRequest() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ return NS_ERROR_ABORT;
+ }
+
+#ifdef DEBUG
+ // Always verify parameters in DEBUG builds!
+ bool trustParams = false;
+#else
+ bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager());
+#endif
+
+ if (!trustParams && NS_WARN_IF(!VerifyRequestParams())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(Start()));
+
+ return NS_OK;
+}
+
+void LSSimpleRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+
+ if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) ||
+ !MayProceed()) {
+ MaybeSetFailureCode(NS_ERROR_ABORT);
+ }
+
+ if (MayProceed()) {
+ LSSimpleRequestResponse response;
+
+ if (NS_SUCCEEDED(ResultCode())) {
+ GetResponse(response);
+ } else {
+ response = ResultCode();
+ }
+
+ Unused << PBackgroundLSSimpleRequestParent::Send__delete__(this, response);
+ }
+
+ mState = State::Completed;
+}
+
+NS_IMETHODIMP
+LSSimpleRequestBase::Run() {
+ nsresult rv;
+
+ switch (mState) {
+ case State::StartingRequest:
+ rv = StartRequest();
+ break;
+
+ case State::SendingResults:
+ SendResults();
+ return NS_OK;
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::SendingResults) {
+ MaybeSetFailureCode(rv);
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ mState = State::SendingResults;
+
+ if (IsOnOwningThread()) {
+ SendResults();
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(
+ OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+ }
+ }
+
+ return NS_OK;
+}
+
+void LSSimpleRequestBase::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ NoteComplete();
+}
+
+/*******************************************************************************
+ * PreloadedOp
+ ******************************************************************************/
+
+PreloadedOp::PreloadedOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSSimpleRequestBase(aParams, aContentParentId) {
+ MOZ_ASSERT(aParams.type() ==
+ LSSimpleRequestParams::TLSSimpleRequestPreloadedParams);
+}
+
+nsresult PreloadedOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSSimpleRequestPreloadedParams& params =
+ mParams.get_LSSimpleRequestPreloadedParams();
+
+ const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo();
+
+ MOZ_ASSERT(
+ storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo ||
+ storagePrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
+ mOrigin = storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo
+ ? nsCString{QuotaManager::GetOriginForChrome()}
+ : QuotaManager::GetOriginFromValidatedPrincipalInfo(
+ storagePrincipalInfo);
+
+ mState = State::SendingResults;
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void PreloadedOp::GetResponse(LSSimpleRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ bool preloaded;
+ RefPtr<Datastore> datastore;
+ if ((datastore = GetDatastore(mOrigin)) && !datastore->IsClosed()) {
+ preloaded = true;
+ } else {
+ preloaded = false;
+ }
+
+ LSSimpleRequestPreloadedResponse preloadedResponse;
+ preloadedResponse.preloaded() = preloaded;
+
+ aResponse = preloadedResponse;
+}
+
+/*******************************************************************************
+ * GetStateOp
+ ******************************************************************************/
+
+GetStateOp::GetStateOp(const LSSimpleRequestParams& aParams,
+ const Maybe<ContentParentId>& aContentParentId)
+ : LSSimpleRequestBase(aParams, aContentParentId) {
+ MOZ_ASSERT(aParams.type() ==
+ LSSimpleRequestParams::TLSSimpleRequestGetStateParams);
+}
+
+nsresult GetStateOp::Start() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::StartingRequest);
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ const LSSimpleRequestGetStateParams& params =
+ mParams.get_LSSimpleRequestGetStateParams();
+
+ const PrincipalInfo& storagePrincipalInfo = params.storagePrincipalInfo();
+
+ MOZ_ASSERT(
+ storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo ||
+ storagePrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo);
+ mOrigin = storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo
+ ? nsCString{QuotaManager::GetOriginForChrome()}
+ : QuotaManager::GetOriginFromValidatedPrincipalInfo(
+ storagePrincipalInfo);
+
+ mState = State::SendingResults;
+ MOZ_ALWAYS_SUCCEEDS(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void GetStateOp::GetResponse(LSSimpleRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State::SendingResults);
+ MOZ_ASSERT(NS_SUCCEEDED(ResultCode()));
+ MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread());
+ MOZ_ASSERT(MayProceed());
+
+ LSSimpleRequestGetStateResponse getStateResponse;
+
+ if (RefPtr<Datastore> datastore = GetDatastore(mOrigin)) {
+ if (!datastore->IsClosed()) {
+ getStateResponse.itemInfos() = datastore->GetOrderedItems().Clone();
+ }
+ }
+
+ aResponse = getStateResponse;
+}
+
+/*******************************************************************************
+ * ArchivedOriginScope
+ ******************************************************************************/
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromOrigin(
+ const nsACString& aOriginAttrSuffix, const nsACString& aOriginKey) {
+ return WrapUnique(
+ new ArchivedOriginScope(Origin(aOriginAttrSuffix, aOriginKey)));
+}
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromPrefix(
+ const nsACString& aOriginKey) {
+ return WrapUnique(new ArchivedOriginScope(Prefix(aOriginKey)));
+}
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromPattern(
+ const OriginAttributesPattern& aPattern) {
+ return WrapUnique(new ArchivedOriginScope(Pattern(aPattern)));
+}
+
+// static
+UniquePtr<ArchivedOriginScope> ArchivedOriginScope::CreateFromNull() {
+ return WrapUnique(new ArchivedOriginScope(Null()));
+}
+
+nsLiteralCString ArchivedOriginScope::GetBindingClause() const {
+ return mData.match(
+ [](const Origin&) {
+ return " WHERE originKey = :originKey "
+ "AND originAttributes = :originAttributes"_ns;
+ },
+ [](const Pattern&) {
+ return " WHERE originAttributes MATCH :originAttributesPattern"_ns;
+ },
+ [](const Prefix&) { return " WHERE originKey = :originKey"_ns; },
+ [](const Null&) { return ""_ns; });
+}
+
+nsresult ArchivedOriginScope::BindToStatement(
+ mozIStorageStatement* aStmt) const {
+ MOZ_ASSERT(IsOnIOThread() || IsOnGlobalConnectionThread());
+ MOZ_ASSERT(aStmt);
+
+ struct Matcher {
+ mozIStorageStatement* mStmt;
+
+ explicit Matcher(mozIStorageStatement* aStmt) : mStmt(aStmt) {}
+
+ nsresult operator()(const Origin& aOrigin) {
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originKey"_ns, aOrigin.OriginNoSuffix())));
+
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originAttributes"_ns, aOrigin.OriginSuffix())));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Prefix& aPrefix) {
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originKey"_ns, aPrefix.OriginNoSuffix())));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Pattern& aPattern) {
+ QM_TRY(MOZ_TO_RESULT(mStmt->BindUTF8StringByName(
+ "originAttributesPattern"_ns, "pattern1"_ns)));
+
+ return NS_OK;
+ }
+
+ nsresult operator()(const Null& aNull) { return NS_OK; }
+ };
+
+ QM_TRY(MOZ_TO_RESULT(mData.match(Matcher(aStmt))));
+
+ return NS_OK;
+}
+
+bool ArchivedOriginScope::HasMatches(
+ ArchivedOriginHashtable* aHashtable) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aHashtable);
+
+ return mData.match(
+ [aHashtable](const Origin& aOrigin) {
+ const nsCString hashKey = GetArchivedOriginHashKey(
+ aOrigin.OriginSuffix(), aOrigin.OriginNoSuffix());
+
+ return aHashtable->Contains(hashKey);
+ },
+ [aHashtable](const Pattern& aPattern) {
+ return std::any_of(
+ aHashtable->Values().cbegin(), aHashtable->Values().cend(),
+ [&aPattern](const auto& entry) {
+ return aPattern.GetPattern().Matches(entry->mOriginAttributes);
+ });
+ },
+ [aHashtable](const Prefix& aPrefix) {
+ return std::any_of(
+ aHashtable->Values().cbegin(), aHashtable->Values().cend(),
+ [&aPrefix](const auto& entry) {
+ return entry->mOriginNoSuffix == aPrefix.OriginNoSuffix();
+ });
+ },
+ [aHashtable](const Null& aNull) { return !aHashtable->IsEmpty(); });
+}
+
+void ArchivedOriginScope::RemoveMatches(
+ ArchivedOriginHashtable* aHashtable) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aHashtable);
+
+ struct Matcher {
+ ArchivedOriginHashtable* mHashtable;
+
+ explicit Matcher(ArchivedOriginHashtable* aHashtable)
+ : mHashtable(aHashtable) {}
+
+ void operator()(const Origin& aOrigin) {
+ nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(),
+ aOrigin.OriginNoSuffix());
+
+ mHashtable->Remove(hashKey);
+ }
+
+ void operator()(const Prefix& aPrefix) {
+ for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) {
+ const auto& archivedOriginInfo = iter.Data();
+
+ if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) {
+ iter.Remove();
+ }
+ }
+ }
+
+ void operator()(const Pattern& aPattern) {
+ for (auto iter = mHashtable->Iter(); !iter.Done(); iter.Next()) {
+ const auto& archivedOriginInfo = iter.Data();
+
+ if (aPattern.GetPattern().Matches(
+ archivedOriginInfo->mOriginAttributes)) {
+ iter.Remove();
+ }
+ }
+ }
+
+ void operator()(const Null& aNull) { mHashtable->Clear(); }
+ };
+
+ mData.match(Matcher(aHashtable));
+}
+
+/*******************************************************************************
+ * QuotaClient
+ ******************************************************************************/
+
+QuotaClient* QuotaClient::sInstance = nullptr;
+
+QuotaClient::QuotaClient()
+ : mShadowDatabaseMutex("LocalStorage mShadowDatabaseMutex") {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!sInstance, "We expect this to be a singleton!");
+
+ sInstance = this;
+}
+
+QuotaClient::~QuotaClient() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(sInstance == this, "We expect this to be a singleton!");
+
+ sInstance = nullptr;
+}
+
+mozilla::dom::quota::Client::Type QuotaClient::GetType() {
+ return QuotaClient::LS;
+}
+
+Result<UsageInfo, nsresult> QuotaClient::InitOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+ MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_INSPECT(const auto& directory,
+ quotaManager->GetOriginDirectory(aOriginMetadata));
+
+ MOZ_ASSERT(directory);
+
+ QM_TRY(MOZ_TO_RESULT(
+ directory->Append(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& directoryPath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, directory, GetPath));
+
+ QM_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath));
+
+ // XXX Try to make usageFileExists const
+ QM_TRY_UNWRAP(bool usageFileExists, ExistsAsFile(*usageFile));
+
+ QM_TRY_INSPECT(const auto& usageJournalFile,
+ GetUsageJournalFile(directoryPath));
+
+ QM_TRY_INSPECT(const bool& usageJournalFileExists,
+ ExistsAsFile(*usageJournalFile));
+
+ if (usageJournalFileExists) {
+ if (usageFileExists) {
+ QM_TRY(MOZ_TO_RESULT(usageFile->Remove(false)));
+
+ usageFileExists = false;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+ }
+
+ QM_TRY_INSPECT(const auto& file,
+ CloneFileAndAppend(*directory, kDataFileName));
+
+ QM_TRY_INSPECT(const bool& fileExists, ExistsAsFile(*file));
+
+ QM_TRY_INSPECT(
+ const UsageInfo& res,
+ ([fileExists, usageFileExists, &file, &usageFile, &usageJournalFile,
+ &aOriginMetadata]() -> Result<UsageInfo, nsresult> {
+ if (fileExists) {
+ QM_TRY_RETURN(QM_OR_ELSE_WARN(
+ // Expression. To simplify control flow, we call LoadUsageFile
+ // unconditionally here, even though it will necessarily fail if
+ // usageFileExists is false.
+ LoadUsageFile(*usageFile),
+ // Fallback.
+ ([&file, &usageFile, &usageJournalFile, &aOriginMetadata](
+ const nsresult) -> Result<UsageInfo, nsresult> {
+ QM_TRY_INSPECT(
+ const auto& connection,
+ CreateStorageConnectionWithRecovery(
+ *file, *usageFile, aOriginMetadata.mOrigin, [] {}));
+
+ QM_TRY_INSPECT(const int64_t& usage,
+ GetUsage(*connection,
+ /* aArchivedOriginScope */ nullptr));
+
+ QM_TRY(MOZ_TO_RESULT(
+ UpdateUsageFile(usageFile, usageJournalFile, usage)));
+
+ QM_TRY(MOZ_TO_RESULT(usageJournalFile->Remove(false)));
+
+ MOZ_ASSERT(usage >= 0);
+ return UsageInfo{DatabaseUsageType(Some(uint64_t(usage)))};
+ })));
+ }
+
+ if (usageFileExists) {
+ QM_TRY(MOZ_TO_RESULT(usageFile->Remove(false)));
+ }
+
+ return UsageInfo{};
+ }()));
+
+ // Report unknown files in debug builds, but don't fail, just warn (we don't
+ // report unknown files in release builds because that requires extra
+ // scanning of the directory which would slow down entire initialization for
+ // little benefit).
+
+#ifdef DEBUG
+ QM_TRY(CollectEachFileAtomicCancelable(
+ *directory, aCanceled,
+ [](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory:
+ Unused << WARN_IF_FILE_IS_UNKNOWN(*file);
+ break;
+
+ case nsIFileKind::ExistsAsFile: {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetLeafName));
+
+ if (leafName.Equals(kDataFileName) ||
+ leafName.Equals(kJournalFileName) ||
+ leafName.Equals(kUsageFileName) ||
+ leafName.Equals(kUsageJournalFileName)) {
+ return Ok{};
+ }
+
+ Unused << WARN_IF_FILE_IS_UNKNOWN(*file);
+
+ break;
+ }
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+ return Ok{};
+ }));
+#endif
+
+ return res;
+}
+
+nsresult QuotaClient::InitOriginWithoutTracking(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+
+ // This is called when a storage/permanent/${origin}/ls directory exists. Even
+ // though this shouldn't happen with a "good" profile, we shouldn't return an
+ // error here, since that would cause origin initialization to fail. We just
+ // warn and otherwise ignore that.
+ UNKNOWN_FILE_WARNING(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME));
+ return NS_OK;
+}
+
+Result<UsageInfo, nsresult> QuotaClient::GetUsageForOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ // We can't open the database at this point, since it can be already used
+ // by the connection thread. Use the cached value instead.
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ return quotaManager->GetUsageForClient(PERSISTENCE_TYPE_DEFAULT,
+ aOriginMetadata, Client::LS);
+}
+
+nsresult QuotaClient::AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope) {
+ AssertIsOnIOThread();
+
+ // This method is not called when the clearing is triggered by the eviction
+ // process. It's on purpose to avoid a problem with the origin access time
+ // which can be described as follows:
+ // When there's a storage pressure condition and quota manager starts
+ // collecting origins for eviction, there can be an origin that hasn't been
+ // touched for long time. However, the old implementation of local storage
+ // could have touched the origin only recently and the new implementation
+ // hasn't had a chance to create a new per origin database for it yet (the
+ // data is still in the archive database), so the origin access time hasn't
+ // been updated either. In the end, the origin would be evicted despite the
+ // fact that there was recent local storage activity.
+ // So this method clears the archived data and shadow database entries for
+ // given origin scope, but only if it's a privacy-related origin clearing.
+
+ if (!aPersistenceType.IsNull() &&
+ aPersistenceType.Value() != PERSISTENCE_TYPE_DEFAULT) {
+ return NS_OK;
+ }
+
+ // There can be no data for the system principal in the archive or the shadow
+ // database. This early return silences potential warnings caused by failed
+ // `CreateAerchivedOriginScope` because it calls `GenerateOriginKey2` which
+ // doesn't support the system principal.
+ if (aOriginScope.IsOrigin() &&
+ aOriginScope.GetOrigin() == QuotaManager::GetOriginForChrome()) {
+ return NS_OK;
+ }
+
+ const bool shadowWrites = gShadowWrites;
+
+ QM_TRY_INSPECT(const auto& archivedOriginScope,
+ CreateArchivedOriginScope(aOriginScope));
+
+ if (!gArchivedOrigins) {
+ QM_TRY(MOZ_TO_RESULT(LoadArchivedOrigins()));
+ MOZ_ASSERT(gArchivedOrigins);
+ }
+
+ const bool hasDataForRemoval =
+ archivedOriginScope->HasMatches(gArchivedOrigins);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ const nsString& basePath = quotaManager->GetBasePath();
+
+ {
+ MutexAutoLock shadowDatabaseLock(mShadowDatabaseMutex);
+
+ QM_TRY_INSPECT(
+ const auto& connection,
+ ([&basePath]() -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> {
+ if (gInitializedShadowStorage) {
+ QM_TRY_RETURN(GetShadowStorageConnection(basePath));
+ }
+
+ QM_TRY_UNWRAP(auto connection,
+ CreateShadowStorageConnection(basePath));
+
+ gInitializedShadowStorage = true;
+
+ return connection;
+ }()));
+
+ {
+ Maybe<AutoDatabaseAttacher> maybeAutoArchiveDatabaseAttacher;
+
+ if (hasDataForRemoval) {
+ QM_TRY_INSPECT(const auto& archiveFile,
+ GetArchiveFile(quotaManager->GetStoragePath()));
+
+ maybeAutoArchiveDatabaseAttacher.emplace(
+ AutoDatabaseAttacher(connection, archiveFile, "archive"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(maybeAutoArchiveDatabaseAttacher->Attach()));
+ }
+
+ if (archivedOriginScope->IsPattern()) {
+ nsCOMPtr<mozIStorageFunction> function(
+ new MatchFunction(archivedOriginScope->GetPattern()));
+
+ QM_TRY(
+ MOZ_TO_RESULT(connection->CreateFunction("match"_ns, 2, function)));
+ }
+
+ {
+ QM_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement, "BEGIN IMMEDIATE;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ if (shadowWrites) {
+ QM_TRY(MOZ_TO_RESULT(
+ PerformDelete(connection, "main"_ns, archivedOriginScope.get())));
+ }
+
+ if (hasDataForRemoval) {
+ QM_TRY(MOZ_TO_RESULT(PerformDelete(connection, "archive"_ns,
+ archivedOriginScope.get())));
+ }
+
+ {
+ QM_TRY_INSPECT(const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, connection,
+ CreateStatement, "COMMIT;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+ }
+
+ if (archivedOriginScope->IsPattern()) {
+ QM_TRY(MOZ_TO_RESULT(connection->RemoveFunction("match"_ns)));
+ }
+
+ if (hasDataForRemoval) {
+ MOZ_ASSERT(maybeAutoArchiveDatabaseAttacher.isSome());
+ QM_TRY(MOZ_TO_RESULT(maybeAutoArchiveDatabaseAttacher->Detach()));
+
+ maybeAutoArchiveDatabaseAttacher.reset();
+
+ MOZ_ASSERT(gArchivedOrigins);
+ MOZ_ASSERT(archivedOriginScope->HasMatches(gArchivedOrigins));
+ archivedOriginScope->RemoveMatches(gArchivedOrigins);
+ }
+ }
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+ }
+
+ if (aOriginScope.IsNull()) {
+ QM_TRY_INSPECT(const auto& shadowFile, GetShadowFile(basePath));
+
+ QM_TRY(MOZ_TO_RESULT(shadowFile->Remove(false)));
+
+ gInitializedShadowStorage = false;
+ }
+
+ return NS_OK;
+}
+
+void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType,
+ const nsACString& aOrigin) {
+ AssertIsOnIOThread();
+}
+
+void QuotaClient::OnRepositoryClearCompleted(PersistenceType aPersistenceType) {
+ AssertIsOnIOThread();
+}
+
+void QuotaClient::ReleaseIOThreadObjects() {
+ AssertIsOnIOThread();
+
+ gInitializationInfo = nullptr;
+
+ // Delete archived origins hashtable since QuotaManager clears the whole
+ // storage directory including ls-archive.sqlite.
+
+ gArchivedOrigins = nullptr;
+}
+
+void QuotaClient::AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) {
+ AssertIsOnBackgroundThread();
+
+ // A PrepareDatastoreOp object could already acquire a directory lock for
+ // the given origin. Its last step is creation of a Datastore object (which
+ // will take ownership of the directory lock) and a PreparedDatastore object
+ // which keeps the Datastore alive until a database actor is created.
+ // We need to invalidate the PreparedDatastore object when it's created,
+ // otherwise the Datastore object can block the origin clear operation for
+ // long time. It's not a problem that we don't fail the PrepareDatastoreOp
+ // immediatelly (avoiding the creation of the Datastore and PreparedDatastore
+ // object). We will call RequestAllowToClose on the database actor once it's
+ // created and the child actor will respond by sending AllowToClose which
+ // will close the Datastore on the parent side (the closing releases the
+ // directory lock).
+
+ InvalidatePrepareDatastoreOpsMatching(
+ [&aDirectoryLockIds](const auto& prepareDatastoreOp) {
+ // Check if the PrepareDatastoreOp holds an acquired DirectoryLock.
+ // Origin clearing can't be blocked by this PrepareDatastoreOp if there
+ // is no acquired DirectoryLock. If there is an acquired DirectoryLock,
+ // check if the table contains the lock for the PrepareDatastoreOp.
+ return IsLockForObjectAcquiredAndContainedInLockTable(
+ prepareDatastoreOp, aDirectoryLockIds);
+ });
+
+ if (gPrivateDatastores) {
+ gPrivateDatastores->RemoveIf([&aDirectoryLockIds](const auto& iter) {
+ const auto& privateDatastore = iter.Data();
+
+ // The PrivateDatastore::mDatastore member is not cleared until the
+ // PrivateDatastore is destroyed.
+ const auto& datastore = privateDatastore->DatastoreRef();
+
+ // If the PrivateDatastore exists then it must be registered in
+ // Datastore::mHasLivePrivateDatastore as well. The Datastore must have
+ // a DirectoryLock if there is a registered PrivateDatastore.
+ return IsLockForObjectContainedInLockTable(datastore, aDirectoryLockIds);
+ });
+
+ if (!gPrivateDatastores->Count()) {
+ gPrivateDatastores = nullptr;
+ }
+ }
+
+ InvalidatePreparedDatastoresMatching([&aDirectoryLockIds](
+ const auto& preparedDatastore) {
+ // The PreparedDatastore::mDatastore member is not cleared until the
+ // PreparedDatastore is destroyed.
+ const auto& datastore = preparedDatastore.DatastoreRef();
+
+ // If the PreparedDatastore exists then it must be registered in
+ // Datastore::mPreparedDatastores as well. The Datastore must have a
+ // DirectoryLock if there are registered PreparedDatastore objects.
+ return IsLockForObjectContainedInLockTable(datastore, aDirectoryLockIds);
+ });
+
+ RequestAllowToCloseDatabasesMatching(
+ [&aDirectoryLockIds](const auto& database) {
+ const auto& maybeDatastore = database.MaybeDatastoreRef();
+
+ // If the Database is registered in gLiveDatabases then it must have a
+ // Datastore.
+ MOZ_ASSERT(maybeDatastore.isSome());
+
+ // If the Database is registered in gLiveDatabases then it must be
+ // registered in Datastore::mDatabases as well. The Datastore must have
+ // a DirectoryLock if there are registered Database objects.
+ return IsLockForObjectContainedInLockTable(*maybeDatastore,
+ aDirectoryLockIds);
+ });
+}
+
+void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) {
+ AssertIsOnBackgroundThread();
+
+ RequestAllowToCloseDatabasesMatching(
+ [&aContentParentId](const auto& database) {
+ return database.IsOwnedByProcess(aContentParentId);
+ });
+}
+
+void QuotaClient::AbortAllOperations() {
+ AssertIsOnBackgroundThread();
+
+ InvalidatePrepareDatastoreOpsMatching([](const auto& prepareDatastoreOp) {
+ return prepareDatastoreOp.MaybeDirectoryLockRef();
+ });
+
+ if (gPrivateDatastores) {
+ gPrivateDatastores = nullptr;
+ }
+
+ InvalidatePreparedDatastoresMatching([](const auto&) { return true; });
+
+ RequestAllowToCloseDatabasesMatching([](const auto&) { return true; });
+}
+
+void QuotaClient::StartIdleMaintenance() { AssertIsOnBackgroundThread(); }
+
+void QuotaClient::StopIdleMaintenance() { AssertIsOnBackgroundThread(); }
+
+void QuotaClient::InitiateShutdown() {
+ // gPrepareDatastoreOps are short lived objects running a state machine.
+ // The shutdown flag is checked between states, so we don't have to notify
+ // all the objects here.
+ // Allocation of a new PrepareDatastoreOp object is prevented once the
+ // shutdown flag is set.
+ // When the last PrepareDatastoreOp finishes, the gPrepareDatastoreOps array
+ // is destroyed.
+
+ if (gPreparedDatastores) {
+ gPreparedDatastores = nullptr;
+ }
+
+ if (gPrivateDatastores) {
+ gPrivateDatastores = nullptr;
+ }
+
+ RequestAllowToCloseDatabasesMatching([](const auto&) { return true; });
+
+ if (gPreparedObsevers) {
+ gPreparedObsevers = nullptr;
+ }
+}
+
+bool QuotaClient::IsShutdownCompleted() const {
+ // Don't have to check gPrivateDatastores and gPreparedDatastores since we
+ // nulled it out in InitiateShutdown.
+ return !gPrepareDatastoreOps && !gDatastores && !gLiveDatabases;
+}
+
+void QuotaClient::ForceKillActors() { ForceKillAllDatabases(); }
+
+nsCString QuotaClient::GetShutdownStatus() const {
+ AssertIsOnBackgroundThread();
+
+ nsCString data;
+
+ if (gPrepareDatastoreOps) {
+ data.Append("PrepareDatastoreOperations: ");
+ data.AppendInt(static_cast<uint32_t>(gPrepareDatastoreOps->Length()));
+ data.Append(" (");
+
+ // 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;
+ std::transform(gPrepareDatastoreOps->cbegin(), gPrepareDatastoreOps->cend(),
+ MakeInserter(ids), [](const auto& prepareDatastoreOp) {
+ nsCString id;
+ prepareDatastoreOp->Stringify(id);
+ return id;
+ });
+
+ StringJoinAppend(data, ", "_ns, ids);
+
+ data.Append(")\n");
+ }
+
+ if (gDatastores) {
+ data.Append("Datastores: ");
+ data.AppendInt(gDatastores->Count());
+ data.Append(" (");
+
+ // XXX It might be confusing to remove duplicates here, as the actual list
+ // won't match the count then.
+ nsTHashSet<nsCString> ids;
+ std::transform(gDatastores->Values().cbegin(), gDatastores->Values().cend(),
+ MakeInserter(ids), [](const auto& entry) {
+ nsCString id;
+ entry->Stringify(id);
+ return id;
+ });
+
+ StringJoinAppend(data, ", "_ns, ids);
+
+ data.Append(")\n");
+ }
+
+ if (gLiveDatabases) {
+ data.Append("LiveDatabases: ");
+ data.AppendInt(static_cast<uint32_t>(gLiveDatabases->Length()));
+ data.Append(" (");
+
+ // XXX It might be confusing to remove duplicates here, as the actual list
+ // won't match the count then.
+ nsTHashSet<nsCString> ids;
+ std::transform(gLiveDatabases->cbegin(), gLiveDatabases->cend(),
+ MakeInserter(ids), [](const auto& database) {
+ nsCString id;
+ database->Stringify(id);
+ return id;
+ });
+
+ StringJoinAppend(data, ", "_ns, ids);
+
+ data.Append(")\n");
+ }
+
+ return data;
+}
+
+void QuotaClient::FinalizeShutdown() {
+ // And finally, shutdown the connection thread.
+ if (gConnectionThread) {
+ gConnectionThread->Shutdown();
+
+ gConnectionThread = nullptr;
+ }
+}
+
+Result<UniquePtr<ArchivedOriginScope>, nsresult>
+QuotaClient::CreateArchivedOriginScope(const OriginScope& aOriginScope) {
+ AssertIsOnIOThread();
+
+ if (aOriginScope.IsOrigin()) {
+ QM_TRY_INSPECT(const auto& principalInfo,
+ QuotaManager::ParseOrigin(aOriginScope.GetOrigin()));
+
+ QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(principalInfo));
+
+ return ArchivedOriginScope::CreateFromOrigin(originAttrSuffix, originKey);
+ }
+
+ if (aOriginScope.IsPrefix()) {
+ QM_TRY_INSPECT(const auto& principalInfo,
+ QuotaManager::ParseOrigin(aOriginScope.GetOriginNoSuffix()));
+
+ QM_TRY_INSPECT((const auto& [originAttrSuffix, originKey]),
+ GenerateOriginKey2(principalInfo));
+
+ Unused << originAttrSuffix;
+
+ return ArchivedOriginScope::CreateFromPrefix(originKey);
+ }
+
+ if (aOriginScope.IsPattern()) {
+ return ArchivedOriginScope::CreateFromPattern(aOriginScope.GetPattern());
+ }
+
+ MOZ_ASSERT(aOriginScope.IsNull());
+
+ return ArchivedOriginScope::CreateFromNull();
+}
+
+nsresult QuotaClient::PerformDelete(
+ mozIStorageConnection* aConnection, const nsACString& aSchemaName,
+ ArchivedOriginScope* aArchivedOriginScope) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(aArchivedOriginScope);
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
+ "DELETE FROM "_ns + aSchemaName + ".webappsstore2"_ns +
+ aArchivedOriginScope->GetBindingClause() + ";"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(aArchivedOriginScope->BindToStatement(stmt)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(QuotaClient::MatchFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+QuotaClient::MatchFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFunctionArguments);
+ MOZ_ASSERT(aResult);
+
+ QM_TRY_INSPECT(const auto& suffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoCString, aFunctionArguments, GetUTF8String, 1));
+
+ OriginAttributes oa;
+ QM_TRY(OkIf(oa.PopulateFromSuffix(suffix)), NS_ERROR_FAILURE);
+
+ const bool result = mPattern.Matches(oa);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ QM_TRY(MOZ_TO_RESULT(outVar->SetAsBool(result)));
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * AutoWriteTransaction
+ ******************************************************************************/
+
+AutoWriteTransaction::AutoWriteTransaction(bool aShadowWrites)
+ : mConnection(nullptr), mShadowWrites(aShadowWrites) {
+ AssertIsOnGlobalConnectionThread();
+
+ MOZ_COUNT_CTOR(mozilla::dom::AutoWriteTransaction);
+}
+
+AutoWriteTransaction::~AutoWriteTransaction() {
+ AssertIsOnGlobalConnectionThread();
+
+ MOZ_COUNT_DTOR(mozilla::dom::AutoWriteTransaction);
+
+ if (mConnection) {
+ QM_WARNONLY_TRY(QM_TO_RESULT(mConnection->RollbackWriteTransaction()));
+
+ if (mShadowWrites) {
+ QM_WARNONLY_TRY(QM_TO_RESULT(DetachShadowDatabaseAndUnlock()));
+ }
+ }
+}
+
+nsresult AutoWriteTransaction::Start(Connection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(!mConnection);
+
+ if (mShadowWrites) {
+ QM_TRY(MOZ_TO_RESULT(LockAndAttachShadowDatabase(aConnection)));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->BeginWriteTransaction()));
+
+ mConnection = aConnection;
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::Commit() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+
+ QM_TRY(MOZ_TO_RESULT(mConnection->CommitWriteTransaction()));
+
+ if (mShadowWrites) {
+ QM_TRY(MOZ_TO_RESULT(DetachShadowDatabaseAndUnlock()));
+ }
+
+ mConnection = nullptr;
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::LockAndAttachShadowDatabase(
+ Connection* aConnection) {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(aConnection);
+ MOZ_ASSERT(!mConnection);
+ MOZ_ASSERT(mShadowDatabaseLock.isNothing());
+ MOZ_ASSERT(mShadowWrites);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ mShadowDatabaseLock.emplace(
+ aConnection->GetQuotaClient()->ShadowDatabaseMutex());
+
+ QM_TRY(MOZ_TO_RESULT(AttachShadowDatabase(
+ quotaManager->GetBasePath(), &aConnection->MutableStorageConnection())));
+
+ return NS_OK;
+}
+
+nsresult AutoWriteTransaction::DetachShadowDatabaseAndUnlock() {
+ AssertIsOnGlobalConnectionThread();
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mShadowDatabaseLock.isSome());
+ MOZ_ASSERT(mShadowWrites);
+
+ nsCOMPtr<mozIStorageConnection> storageConnection =
+ mConnection->StorageConnection();
+ MOZ_ASSERT(storageConnection);
+
+ QM_TRY(MOZ_TO_RESULT(DetachShadowDatabase(storageConnection)));
+
+ mShadowDatabaseLock.reset();
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom