diff options
Diffstat (limited to 'dom/localstorage/ActorsParent.cpp')
-rw-r--r-- | dom/localstorage/ActorsParent.cpp | 9014 |
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(¤t)) && + 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(¤t)) && 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, "aManager, + 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(), ["aObject, 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(["aObject] { + 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 |