diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/localstorage/ActorsParent.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/localstorage/ActorsParent.cpp')
-rw-r--r-- | dom/localstorage/ActorsParent.cpp | 9059 |
1 files changed, 9059 insertions, 0 deletions
diff --git a/dom/localstorage/ActorsParent.cpp b/dom/localstorage/ActorsParent.cpp new file mode 100644 index 0000000000..b5c3f03336 --- /dev/null +++ b/dom/localstorage/ActorsParent.cpp @@ -0,0 +1,9059 @@ +/* -*- 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 "LSObject.h" +#include "ReportInternalError.h" + +// Global includes +#include <cinttypes> +#include <cstdlib> +#include <cstring> +#include <new> +#include <tuple> +#include <type_traits> +#include <utility> +#include "CrashAnnotations.h" +#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/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/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/OriginScope.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/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 "nsDataHashtable.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsExceptionHandler.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 "nsTHashtable.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 DISABLE_ASSERTS_FOR_FUZZING 0 + +#if DISABLE_ASSERTS_FOR_FUZZING +# define ASSERT_UNLESS_FUZZING(...) \ + do { \ + } while (0) +#else +# define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__) +#endif + +#define 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; + +typedef nsClassHashtable<nsCStringHashKey, ArchivedOriginInfo> + ArchivedOriginHashtable; + +/******************************************************************************* + * Constants + ******************************************************************************/ + +// Major schema version. Bump for almost everything. +const uint32_t kMajorSchemaVersion = 4; + +// 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.) + */ +constexpr auto kJournalFileName = u"data.sqlite-journal"_ns; + +/** + * 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 char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited"; + +const uint32_t kDefaultShadowWrites = true; +const uint32_t kDefaultSnapshotPrefill = 16384; +const uint32_t kDefaultSnapshotGradualPrefill = 4096; +const uint32_t 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; + +const uint32_t kShadowJournalSizeLimit = kShadowMaxWALSize * 3; + +bool IsOnConnectionThread(); + +void AssertIsOnConnectionThread(); + +/******************************************************************************* + * 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 CreateTables(mozIStorageConnection* aConnection) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(aConnection); + + // Table `database` + LS_TRY(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` + LS_TRY(aConnection->ExecuteSimpleSQL( + "CREATE TABLE data" + "( key TEXT PRIMARY KEY" + ", value TEXT NOT NULL" + ", utf16Length INTEGER NOT NULL DEFAULT 0" + ", compressed INTEGER NOT NULL DEFAULT 0" + ", lastAccessTime INTEGER NOT NULL DEFAULT 0" + ");"_ns)); + + LS_TRY(aConnection->SetSchemaVersion(kSQLiteSchemaVersion)); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom1_0To2_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + LS_TRY(aConnection->ExecuteSimpleSQL( + "ALTER TABLE database ADD COLUMN usage INTEGER NOT NULL DEFAULT 0;"_ns)); + + LS_TRY(aConnection->ExecuteSimpleSQL( + "UPDATE database " + "SET usage = (SELECT total(utf16Length(key) + utf16Length(value)) " + "FROM data);"_ns)); + + LS_TRY(aConnection->SetSchemaVersion(MakeSchemaVersion(2, 0))); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom2_0To3_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + LS_TRY(aConnection->ExecuteSimpleSQL( + "ALTER TABLE data ADD COLUMN utf16Length INTEGER NOT NULL DEFAULT 0;"_ns)); + + LS_TRY(aConnection->ExecuteSimpleSQL( + "UPDATE data SET utf16Length = utf16Length(value);"_ns)); + + LS_TRY(aConnection->SetSchemaVersion(MakeSchemaVersion(3, 0))); + + return NS_OK; +} + +nsresult UpgradeSchemaFrom3_0To4_0(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + LS_TRY(aConnection->SetSchemaVersion(MakeSchemaVersion(4, 0))); + + return NS_OK; +} + +nsresult SetDefaultPragmas(mozIStorageConnection* aConnection) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aConnection); + + LS_TRY(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. + LS_TRY( + ToResult(aConnection->SetGrowthIncrement(kSQLiteGrowthIncrement, ""_ns)) + .orElse(ErrToDefaultOkOrErr<NS_ERROR_FILE_TOO_BIG, Ok>)); + } +#endif // LS_MOBILE + + return NS_OK; +} + +template <typename CorruptedFileHandler> +Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateStorageConnection( + nsIFile& aDBFile, nsIFile& aUsageFile, const nsACString& aOrigin, + CorruptedFileHandler&& aCorruptedFileHandler) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + + // XXX Common logic should be refactored out of this method and + // cache::DBAction::OpenDBConnection, and maybe other similar functions. + + LS_TRY_INSPECT( + const auto& storageService, + ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), MOZ_STORAGE_SERVICE_CONTRACTID)); + + LS_TRY_UNWRAP( + auto connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, + storageService, OpenDatabase, &aDBFile) + .orElse([&aUsageFile, &aDBFile, &aCorruptedFileHandler, + &storageService](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (rv == NS_ERROR_FILE_CORRUPTED) { + // Remove the usage file first (it might not exist at all due + // to corrupted state, which is ignored here). + LS_TRY(ToResult(aUsageFile.Remove(false)) + .orElse([](const nsresult rv) -> Result<Ok, nsresult> { + if (rv == NS_ERROR_FILE_NOT_FOUND || + rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { + return Ok{}; + } + + return Err(rv); + })); + + // Call the corrupted file handler before trying to remove the + // database file, which might fail. + std::forward<CorruptedFileHandler>(aCorruptedFileHandler)(); + + // Nuke the database file. + LS_TRY(aDBFile.Remove(false)); + + LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageConnection>, storageService, OpenDatabase, + &aDBFile)); + } + return Err(rv); + })); + + LS_TRY(SetDefaultPragmas(connection)); + + // Check to make sure that the database schema is correct. + // XXX Try to make schemaVersion const. + LS_TRY_UNWRAP(int32_t schemaVersion, + MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion)); + + LS_TRY(OkIf(schemaVersion <= kSQLiteSchemaVersion), Err(NS_ERROR_FAILURE)); + + if (schemaVersion != kSQLiteSchemaVersion) { + const bool newDatabase = !schemaVersion; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + LS_TRY(connection->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride))); + } + + // We have to set the auto_vacuum mode before opening a transaction. + LS_TRY(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 + )); + } + + mozStorageTransaction transaction( + connection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + if (newDatabase) { + LS_TRY(CreateTables(connection)); + +#ifdef DEBUG + { + LS_TRY_INSPECT(const int32_t& schemaVersion, + MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion), + QM_ASSERT_UNREACHABLE); + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } +#endif + + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "INSERT INTO database (origin) VALUES (:origin)"_ns)); + + LS_TRY(stmt->BindUTF8StringByName("origin"_ns, aOrigin)); + + LS_TRY(stmt->Execute()); + } else { + // This logic needs to change next time we change the schema! + static_assert(kSQLiteSchemaVersion == int32_t((4 << 4) + 0), + "Upgrade function needed due to schema version increase."); + + while (schemaVersion != kSQLiteSchemaVersion) { + if (schemaVersion == MakeSchemaVersion(1, 0)) { + LS_TRY(UpgradeSchemaFrom1_0To2_0(connection)); + } else if (schemaVersion == MakeSchemaVersion(2, 0)) { + LS_TRY(UpgradeSchemaFrom2_0To3_0(connection)); + } else if (schemaVersion == MakeSchemaVersion(3, 0)) { + LS_TRY(UpgradeSchemaFrom3_0To4_0(connection)); + } else { + LS_WARNING( + "Unable to open LocalStorage database, no upgrade path is " + "available!"); + return Err(NS_ERROR_FAILURE); + } + + LS_TRY_UNWRAP(schemaVersion, + MOZ_TO_RESULT_INVOKE(connection, GetSchemaVersion)); + } + + MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion); + } + + LS_TRY(transaction.Commit()); + + if (newDatabase) { + // Windows caches the file size, let's force it to stat the file again. + LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aDBFile, Exists)); + Unused << exists; + + LS_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE(aDBFile, GetFileSize)); + + MOZ_ASSERT(fileSize > 0); + + const PRTime vacuumTime = PR_Now(); + MOZ_ASSERT(vacuumTime); + + LS_TRY_INSPECT( + const auto& vacuumTimeStmt, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection, + CreateStatement, + "UPDATE database " + "SET last_vacuum_time = :time" + ", last_vacuum_size = :size;"_ns)); + + LS_TRY(vacuumTimeStmt->BindInt64ByName("time"_ns, vacuumTime)); + + LS_TRY(vacuumTimeStmt->BindInt64ByName("size"_ns, fileSize)); + + LS_TRY(vacuumTimeStmt->Execute()); + } + } + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetStorageConnection( + const nsAString& aDatabaseFilePath) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, u".sqlite"_ns)); + + LS_TRY_INSPECT(const auto& databaseFile, QM_NewLocalFile(aDatabaseFilePath)); + + LS_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(databaseFile, Exists)); + + LS_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + LS_TRY_UNWRAP(auto connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenDatabase, databaseFile)); + + LS_TRY(SetDefaultPragmas(connection)); + + return connection; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetArchiveFile( + const nsAString& aStoragePath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + + LS_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aStoragePath)); + + LS_TRY(archiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))); + + return archiveFile; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +CreateArchiveStorageConnection(const nsAString& aStoragePath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + + LS_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); + + LS_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(archiveFile, IsDirectory)); + + if (isDirectory) { + LS_WARNING("ls-archive is not a file!"); + return nsCOMPtr<mozIStorageConnection>{}; + } + + LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + LS_TRY_UNWRAP( + auto connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, archiveFile) + .orElse([](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (rv == NS_ERROR_FILE_CORRUPTED) { + // Don't throw an error, leave a corrupted ls-archive database as + // it is. + return nsCOMPtr<mozIStorageConnection>{}; + } + + return Err(rv); + })); + + 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; +} + +nsresult AttachArchiveDatabase(const nsAString& aStoragePath, + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aStoragePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + LS_TRY_INSPECT(const auto& archiveFile, GetArchiveFile(aStoragePath)); + +#ifdef DEBUG + { + LS_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(archiveFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + LS_TRY_INSPECT(const auto& path, + MOZ_TO_RESULT_INVOKE_TYPED(nsString, archiveFile, GetPath)); + + LS_TRY_INSPECT(const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, "ATTACH DATABASE :path AS archive;"_ns)); + + LS_TRY(stmt->BindStringByName("path"_ns, path)); + LS_TRY(stmt->Execute()); + + return NS_OK; +} + +nsresult DetachArchiveDatabase(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + LS_TRY(aConnection->ExecuteSimpleSQL("DETACH DATABASE archive"_ns)); + + return NS_OK; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetShadowFile(const nsAString& aBasePath) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(!aBasePath.IsEmpty()); + + LS_TRY_UNWRAP(auto archiveFile, QM_NewLocalFile(aBasePath)); + + LS_TRY(archiveFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME))); + + return archiveFile; +} + +nsresult SetShadowJournalMode(mozIStorageConnection* aConnection) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + 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; + + LS_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + *aConnection, journalModeQueryStart + journalModeWAL)); + + LS_TRY_INSPECT( + const auto& journalMode, + MOZ_TO_RESULT_INVOKE_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. + LS_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement( + *aConnection, "PRAGMA page_size;"_ns)); + + LS_TRY_INSPECT(const int32_t& pageSize, + MOZ_TO_RESULT_INVOKE(*stmt, GetInt32, 0)); + + MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536); + + LS_TRY(aConnection->ExecuteSimpleSQL( + "PRAGMA wal_autocheckpoint = "_ns + + IntToCString(static_cast<int32_t>(kShadowMaxWALSize / pageSize)))); + + // Set the maximum WAL log size to reduce footprint on mobile (large empty + // WAL files will be truncated) + LS_TRY( + aConnection->ExecuteSimpleSQL("PRAGMA journal_size_limit = "_ns + + IntToCString(kShadowJournalSizeLimit))); + } else { + LS_TRY( + aConnection->ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns)); + } + + return NS_OK; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateShadowStorageConnection( + const nsAString& aBasePath) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(!aBasePath.IsEmpty()); + + LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath)); + + LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + LS_TRY_UNWRAP( + auto connection, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, shadowFile) + .orElse([&shadowFile, &ss](const nsresult rv) + -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (rv == NS_ERROR_FILE_CORRUPTED) { + LS_TRY(shadowFile->Remove(false)); + + LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + shadowFile)); + } + + return Err(rv); + })); + + LS_TRY(SetShadowJournalMode(connection)); + + LS_TRY(ToResult(StorageDBUpdater::Update(connection)) + .orElse([&connection, &shadowFile, + &ss](const nsresult) -> Result<Ok, nsresult> { + LS_TRY(connection->Close()); + LS_TRY(shadowFile->Remove(false)); + + LS_TRY_UNWRAP(connection, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, shadowFile)); + + LS_TRY(SetShadowJournalMode(connection)); + + LS_TRY(StorageDBUpdater::Update(connection)); + + return Ok{}; + })); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> GetShadowStorageConnection( + const nsAString& aBasePath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + + LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath)); + + LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(shadowFile, Exists)); + + LS_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + LS_TRY_INSPECT(const auto& ss, ToResultGet<nsCOMPtr<mozIStorageService>>( + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, shadowFile)); +} + +nsresult AttachShadowDatabase(const nsAString& aBasePath, + mozIStorageConnection* aConnection) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aBasePath.IsEmpty()); + MOZ_ASSERT(aConnection); + + LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(aBasePath)); + +#ifdef DEBUG + { + LS_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(shadowFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + LS_TRY_INSPECT(const auto& path, + MOZ_TO_RESULT_INVOKE_TYPED(nsString, shadowFile, GetPath)); + + LS_TRY_INSPECT(const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, + CreateStatement, "ATTACH DATABASE :path AS shadow;"_ns)); + + LS_TRY(stmt->BindStringByName("path"_ns, path)); + + LS_TRY(stmt->Execute()); + + return NS_OK; +} + +nsresult DetachShadowDatabase(mozIStorageConnection* aConnection) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + LS_TRY(aConnection->ExecuteSimpleSQL("DETACH DATABASE shadow"_ns)); + + return NS_OK; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetUsageFile( + const nsAString& aDirectoryPath) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + LS_TRY_UNWRAP(auto usageFile, QM_NewLocalFile(aDirectoryPath)); + + LS_TRY(usageFile->Append(kUsageFileName)); + + return usageFile; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetUsageJournalFile( + const nsAString& aDirectoryPath) { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + LS_TRY_UNWRAP(auto usageJournalFile, QM_NewLocalFile(aDirectoryPath)); + + LS_TRY(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. + LS_TRY_INSPECT( + const auto& res, + MOZ_TO_RESULT_INVOKE(aFile, IsDirectory) + .map([](const bool isDirectory) { + return isDirectory ? ExistsAsFileResult::IsDirectory + : ExistsAsFileResult::IsFile; + }) + .orElse( + [](const nsresult rv) -> Result<ExistsAsFileResult, nsresult> { + if (rv != NS_ERROR_FILE_NOT_FOUND && + rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { + return Err(rv); + } + return ExistsAsFileResult::DoesNotExist; + })); + + LS_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() || IsOnConnectionThread()); + MOZ_ASSERT(aUsageFile); + MOZ_ASSERT(aUsageJournalFile); + MOZ_ASSERT(aUsage >= 0); + + LS_TRY_INSPECT(const bool& usageJournalFileExists, + ExistsAsFile(*aUsageJournalFile)); + if (!usageJournalFileExists) { + LS_TRY(aUsageJournalFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)); + } + + LS_TRY_INSPECT(const auto& stream, NS_NewLocalFileOutputStream(aUsageFile)); + + nsCOMPtr<nsIBinaryOutputStream> binaryStream = + NS_NewObjectOutputStream(stream); + + LS_TRY(binaryStream->Write32(kUsageFileCookie)); + + LS_TRY(binaryStream->Write64(aUsage)); + + LS_TRY(stream->Close()); + + return NS_OK; +} + +Result<UsageInfo, nsresult> LoadUsageFile(nsIFile& aUsageFile) { + AssertIsOnIOThread(); + + LS_TRY_INSPECT(const int64_t& fileSize, + MOZ_TO_RESULT_INVOKE(aUsageFile, GetFileSize)); + + LS_TRY(OkIf(fileSize == kUsageFileSize), Err(NS_ERROR_FILE_CORRUPTED)); + + LS_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(&aUsageFile)); + + LS_TRY_INSPECT(const auto& bufferedStream, + NS_NewBufferedInputStream(stream.forget(), 16)); + + const nsCOMPtr<nsIBinaryInputStream> binaryStream = + NS_NewObjectInputStream(bufferedStream); + + LS_TRY_INSPECT(const uint32_t& cookie, + MOZ_TO_RESULT_INVOKE(binaryStream, Read32)); + + LS_TRY(OkIf(cookie == kUsageFileCookie), Err(NS_ERROR_FILE_CORRUPTED)); + + LS_TRY_INSPECT(const uint64_t& usage, + MOZ_TO_RESULT_INVOKE(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(GetCurrentEventTarget()), + 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 { + friend class ConnectionThread; + + public: + class CachedStatement; + + private: + class InitTemporaryOriginHelper; + + class FlushOp; + class CloseOp; + + RefPtr<ConnectionThread> mConnectionThread; + RefPtr<QuotaClient> mQuotaClient; + nsCOMPtr<nsITimer> mFlushTimer; + nsCOMPtr<mozIStorageConnection> mStorageConnection; + UniquePtr<ArchivedOriginScope> mArchivedOriginScope; + nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement> + mCachedStatements; + ConnectionWriteOptimizer mWriteOptimizer; + const QuotaInfo mQuotaInfo; + 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 mQuotaInfo.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 { + AssertIsOnConnectionThread(); + + return mStorageConnection; + } + + void CloseStorageConnection(); + + nsresult GetCachedStatement(const nsACString& aQuery, + CachedStatement* aCachedStatement); + + nsresult BeginWriteTransaction(); + + nsresult CommitWriteTransaction(); + + nsresult RollbackWriteTransaction(); + + private: + // Only created by ConnectionThread. + Connection(ConnectionThread* aConnectionThread, const QuotaInfo& aQuotaInfo, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable); + + ~Connection(); + + void ScheduleFlush(); + + void Flush(); + + static void FlushTimerCallback(nsITimer* aTimer, void* aClosure); +}; + +class Connection::CachedStatement final { + friend class Connection; + + nsCOMPtr<mozIStorageStatement> mStatement; + Maybe<mozStorageStatementScoper> mScoper; + + public: + CachedStatement(); + ~CachedStatement(); + + operator mozIStorageStatement*() const; + + mozIStorageStatement* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN; + + private: + // Only called by Connection. + void Assign(Connection* aConnection, + already_AddRefed<mozIStorageStatement> aStatement); + + // No funny business allowed. + CachedStatement(const CachedStatement&) = delete; + CachedStatement& operator=(const CachedStatement&) = delete; +}; + +/** + * 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; + const QuotaInfo mQuotaInfo; + nsString mOriginDirectoryPath; + nsresult mIOThreadResultCode; + bool mWaiting; + + public: + explicit InitTemporaryOriginHelper(const QuotaInfo& aQuotaInfo) + : Runnable("dom::localstorage::Connection::InitTemporaryOriginHelper"), + mMonitor("InitTemporaryOriginHelper::mMonitor"), + mQuotaInfo(aQuotaInfo), + mIOThreadResultCode(NS_OK), + mWaiting(true) { + AssertIsOnConnectionThread(); + } + + 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 QuotaInfo& aQuotaInfo, + 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. + */ + nsTHashtable<nsPtrHashKey<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. + */ + nsTHashtable<nsPtrHashKey<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. + */ + nsTHashtable<nsPtrHashKey<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. + */ + nsTHashtable<nsPtrHashKey<Database>> mActiveDatabases; + /** + * Non-authoritative hashtable representation of mOrderedItems for efficient + * lookup. + */ + nsDataHashtable<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 GroupAndOrigin mGroupAndOrigin; + 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 GroupAndOrigin& aGroupAndOrigin, uint32_t aPrivateBrowsingId, + int64_t aUsage, int64_t aSizeOfKeys, int64_t aSizeOfItems, + RefPtr<DirectoryLock>&& aDirectoryLock, + RefPtr<Connection>&& aConnection, + RefPtr<QuotaObject>&& aQuotaObject, + nsDataHashtable<nsStringHashKey, LSValue>& aValues, + nsTArray<LSItemInfo>&& aOrderedItems); + + Maybe<DirectoryLock&> MaybeDirectoryLockRef() const { + AssertIsOnBackgroundThread(); + + return ToMaybeRef(mDirectoryLock.get()); + } + + const nsCString& Origin() const { return mGroupAndOrigin.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); + + void NoteLiveDatabase(Database* aDatabase); + + void NoteFinishedDatabase(Database* aDatabase); + + void NoteActiveDatabase(Database* aDatabase); + + void NoteInactiveDatabase(Database* aDatabase); + + void GetSnapshotLoadInfo(const nsString& 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 nsString& aKey, LSValue& aValue) const; + + void GetKeys(nsTArray<nsString>& aKeys) const; + + ////////////////////////////////////////////////////////////////////////////// + // Mutation Methods + // + // These are only called during Snapshot::RecvCheckpoint + + /** + * Used by Snapshot::RecvCheckpoint to set a key/value pair as part of a 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 aSnapshotInitialUsage); + + int64_t EndUpdateBatch(int64_t aSnapshotPeakUsage); + + int64_t GetUsage() const { return mUsage; } + + int64_t RequestUpdateUsage(int64_t aRequestedSize, int64_t aMinSize); + + 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); +}; + +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) + + 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 nsString& aDocumentURI, const nsString& aKey, + const bool& aIncreasePeakUsage, const int64_t& aRequestedSize, + const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) override; + + mozilla::ipc::IPCResult RecvPBackgroundLSSnapshotConstructor( + PBackgroundLSSnapshotParent* aActor, const nsString& aDocumentURI, + const nsString& aKey, const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, 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.) + */ + nsTHashtable<nsStringHashKey> 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. + */ + nsDataHashtable<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; + + bool mHasOtherProcessObservers; + + public: + // Created in AllocPBackgroundLSSnapshotParent. + Snapshot(Database* aDatabase, const nsAString& aDocumentURI); + + void Init(nsTHashtable<nsStringHashKey>& aLoadedItems, + nsTHashtable<nsStringHashKey>& aUnknownItems, + uint32_t aNextLoadIndex, uint32_t aTotalLength, + int64_t aInitialUsage, int64_t aPeakUsage, + LSSnapshot::LoadState aLoadState, bool aHasOtherProcessObservers) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aInitialUsage >= 0); + MOZ_ASSERT(aPeakUsage >= aInitialUsage); + 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.SwapElements(aUnknownItems); + mNextLoadIndex = aNextLoadIndex; + mTotalLength = aTotalLength; + mUsage = aInitialUsage; + 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; + } + 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 HasOtherProcessObservers() const { + AssertIsOnBackgroundThread(); + + return mHasOtherProcessObservers; + } + + NS_INLINE_DECL_REFCOUNTING(mozilla::dom::Snapshot) + + private: + // Reference counted. + ~Snapshot(); + + void Finish(); + + // IPDL methods are only called by IPDL. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDeleteMe() override; + + mozilla::ipc::IPCResult RecvCheckpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) override; + + mozilla::ipc::IPCResult RecvCheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) override; + + mozilla::ipc::IPCResult RecvFinish() override; + + mozilla::ipc::IPCResult RecvLoaded() override; + + mozilla::ipc::IPCResult RecvLoadValueAndMoreItems( + const nsString& aKey, LSValue* aValue, + nsTArray<LSItemInfo>* aItemInfos) override; + + mozilla::ipc::IPCResult RecvLoadKeys(nsTArray<nsString>* aKeys) override; + + mozilla::ipc::IPCResult RecvIncreasePeakUsage(const int64_t& aRequestedSize, + const int64_t& aMinSize, + int64_t* aSize) override; + + mozilla::ipc::IPCResult RecvPing() 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 + }; + + nsCOMPtr<nsIEventTarget> mMainEventTarget; + const LSRequestParams mParams; + Maybe<ContentParentId> mContentParentId; + State mState; + bool mWaitingForFinish; + + public: + LSRequestBase(nsIEventTarget* aMainEventTarget, + 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 OpenDirectoryListener, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + class LoadDataOp; + + class CompressFunction; + class CompressibleFunction; + + 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, + + // Opening directory or initializing quota manager on the PBackground + // thread. Next step is either DirectoryOpenPending if quota manager is + // already initialized or QuotaManagerPending if quota manager needs to be + // initialized. + // If a datastore already exists for given origin then the next state is + // SendingReadyMessage. + PreparationPending, + + // Waiting for quota manager initialization to complete on the PBackground + // thread. Next step is either SendingReadyMessage if initialization failed + // or DirectoryOpenPending if initialization succeeded. + QuotaManagerPending, + + // 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 + }; + + nsCOMPtr<nsIEventTarget> mMainEventTarget; + RefPtr<PrepareDatastoreOp> mDelayedOp; + RefPtr<DirectoryLock> mPendingDirectoryLock; + RefPtr<DirectoryLock> mDirectoryLock; + RefPtr<Connection> mConnection; + RefPtr<Datastore> mDatastore; + UniquePtr<ArchivedOriginScope> mArchivedOriginScope; + LoadDataOp* mLoadDataOp; + nsDataHashtable<nsStringHashKey, LSValue> mValues; + nsTArray<LSItemInfo> mOrderedItems; + QuotaInfo mQuotaInfo; + 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(nsIEventTarget* aMainEventTarget, + const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId); + + Maybe<DirectoryLock&> MaybeDirectoryLockRef() const { + AssertIsOnBackgroundThread(); + + return ToMaybeRef(mDirectoryLock.get()); + } + + bool OriginIsKnown() const { + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + + return !mQuotaInfo.mOrigin.IsEmpty(); + } + + const nsCString& Origin() const { + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + MOZ_ASSERT(OriginIsKnown()); + + return mQuotaInfo.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(); + + nsresult QuotaManagerOpen(); + + nsresult OpenDirectory(); + + 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(); + + NS_DECL_ISUPPORTS_INHERITED + + // IPDL overrides. + void ActorDestroy(ActorDestroyReason aWhy) override; + + // OpenDirectoryListener overrides. + void DirectoryLockAcquired(DirectoryLock* aLock) override; + + void DirectoryLockFailed() override; +}; + +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::CompressibleFunction final + : public mozIStorageFunction { + private: + ~CompressibleFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +class PrepareObserverOp : public LSRequestBase { + nsCString mOrigin; + + public: + PrepareObserverOp(nsIEventTarget* aMainEventTarget, + 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; +}; + +/******************************************************************************* + * 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 Observer; + class MatchFunction; + + static QuotaClient* sInstance; + + Mutex mShadowDatabaseMutex; + bool mShutdownRequested; + + public: + QuotaClient(); + + static nsresult Initialize(); + + static QuotaClient* GetInstance() { + AssertIsOnBackgroundThread(); + + return sInstance; + } + + static bool IsShuttingDownOnBackgroundThread() { + AssertIsOnBackgroundThread(); + + if (sInstance) { + return sInstance->IsShuttingDown(); + } + + return QuotaManager::IsShuttingDown(); + } + + static bool IsShuttingDownOnNonBackgroundThread() { + MOZ_ASSERT(!IsOnBackgroundThread()); + + return QuotaManager::IsShuttingDown(); + } + + mozilla::Mutex& ShadowDatabaseMutex() { + MOZ_ASSERT(IsOnIOThread() || IsOnConnectionThread()); + + return mShadowDatabaseMutex; + } + + bool IsShuttingDown() const { + AssertIsOnBackgroundThread(); + + return mShutdownRequested; + } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::QuotaClient, override) + + Type GetType() override; + + Result<UsageInfo, nsresult> InitOrigin(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, + const AtomicBool& aCanceled) override; + + nsresult InitOriginWithoutTracking(PersistenceType aPersistenceType, + const GroupAndOrigin& aGroupAndOrigin, + const AtomicBool& aCanceled) override; + + Result<UsageInfo, nsresult> GetUsageForOrigin( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin, + const AtomicBool& aCanceled) override; + + nsresult AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope) override; + + void OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) 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::Observer final : public nsIObserver { + public: + static nsresult Initialize(); + + private: + Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + ~Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + nsresult Init(); + + nsresult Shutdown(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER +}; + +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. +typedef std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM, + nsCStringHashKey>::type DatastoreHashKey; + +typedef nsDataHashtable<DatastoreHashKey, CheckedUnsafePtr<Datastore>> + DatastoreHashtable; + +StaticAutoPtr<DatastoreHashtable> gDatastores; + +uint64_t gLastDatastoreId = 0; + +typedef nsClassHashtable<nsUint64HashKey, PreparedDatastore> + PreparedDatastoreHashtable; + +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; + +typedef nsRefPtrHashtable<nsUint64HashKey, Observer> PreparedObserverHashtable; + +StaticAutoPtr<PreparedObserverHashtable> gPreparedObsevers; + +typedef nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<Observer*>>> + ObserverHashtable; + +StaticAutoPtr<ObserverHashtable> gObservers; + +Atomic<bool> gShadowWrites(kDefaultShadowWrites); +Atomic<int32_t, Relaxed> gSnapshotPrefill(kDefaultSnapshotPrefill); +Atomic<int32_t, Relaxed> gSnapshotGradualPrefill( + kDefaultSnapshotGradualPrefill); +Atomic<bool> gClientValidation(kDefaultClientValidation); + +typedef nsDataHashtable<nsCStringHashKey, int64_t> UsageHashtable; + +StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins; + +// Can only be touched on the Quota Manager I/O thread. +bool gInitializedShadowStorage = false; + +bool IsOnConnectionThread() { + MOZ_ASSERT(gConnectionThread); + return gConnectionThread->IsOnConnectionThread(); +} + +void AssertIsOnConnectionThread() { + MOZ_ASSERT(gConnectionThread); + gConnectionThread->AssertIsOnConnectionThread(); +} + +already_AddRefed<Datastore> GetDatastore(const nsACString& aOrigin) { + AssertIsOnBackgroundThread(); + + if (gDatastores) { + CheckedUnsafePtr<Datastore> datastore; + if (gDatastores->Get(aOrigin, &datastore)) { + MOZ_ASSERT(datastore); + + RefPtr<Datastore> result(datastore); + return result.forget(); + } + } + + return nullptr; +} + +nsresult LoadArchivedOrigins() { + AssertIsOnIOThread(); + MOZ_ASSERT(!gArchivedOrigins); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Ensure that the webappsstore.sqlite is moved to new place. + LS_TRY(quotaManager->EnsureStorageIsInitialized()); + + LS_TRY_INSPECT(const auto& connection, CreateArchiveStorageConnection( + quotaManager->GetStoragePath())); + + if (!connection) { + gArchivedOrigins = new ArchivedOriginHashtable(); + return NS_OK; + } + + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_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 + LS_TRY(quota::CollectWhileHasResult( + *stmt, [&archivedOrigins](auto& stmt) -> Result<Ok, nsresult> { + LS_TRY_INSPECT( + const auto& originSuffix, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0)); + LS_TRY_INSPECT( + const auto& originNoSuffix, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 1)); + + const nsCString hashKey = + GetArchivedOriginHashKey(originSuffix, originNoSuffix); + + OriginAttributes originAttributes; + LS_TRY(OkIf(originAttributes.PopulateFromSuffix(originSuffix)), + Err(NS_ERROR_FAILURE)); + + archivedOrigins->Put(hashKey, MakeUnique<ArchivedOriginInfo>( + originAttributes, originNoSuffix)); + + return Ok{}; + })); + + gArchivedOrigins = archivedOrigins.release(); + return NS_OK; +} + +Result<int64_t, nsresult> GetUsage(mozIStorageConnection& aConnection, + ArchivedOriginScope* aArchivedOriginScope) { + AssertIsOnIOThread(); + + LS_TRY_INSPECT( + const auto& stmt, + ([aArchivedOriginScope, + &aConnection]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> { + if (aArchivedOriginScope) { + LS_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> { + LS_TRY(aArchivedOriginScope->BindToStatement(&stmt)); + return Ok{}; + })); + } + + LS_TRY_RETURN(CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT usage FROM database"_ns)); + }())); + + LS_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE)); + + LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE(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; +} + +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& preparedDatastoreEntry : *gPreparedDatastores) { + const auto& preparedDatastore = preparedDatastoreEntry.GetData(); + 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) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) { + return false; + } + + if (NS_WARN_IF(!StoragePrincipalHelper:: + VerifyValidStoragePrincipalInfoForPrincipalInfo( + aStoragePrincipalInfo, aPrincipalInfo))) { + return false; + } + + return true; +} + +bool VerifyClientId(const Maybe<ContentParentId>& aContentParentId, + const PrincipalInfo& aPrincipalInfo, + const Maybe<nsID>& aClientId) { + AssertIsOnBackgroundThread(); + + if (gClientValidation) { + if (NS_WARN_IF(aClientId.isNothing())) { + return false; + } + + RefPtr<ClientManagerService> svc = ClientManagerService::GetInstance(); + if (svc && NS_WARN_IF(!svc->HasWindow(aContentParentId, aPrincipalInfo, + aClientId.ref()))) { + return false; + } + } + + return true; +} + +bool VerifyOriginKey(const nsACString& aOriginKey, + const PrincipalInfo& aPrincipalInfo) { + AssertIsOnBackgroundThread(); + + LS_TRY_INSPECT((const auto& [originAttrSuffix, originKey]), + GenerateOriginKey2(aPrincipalInfo), false); + + Unused << originAttrSuffix; + + LS_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; +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +void InitializeLocalStorage() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gLocalStorageInitialized); + + if (!QuotaManager::IsRunningGTests()) { + // This service has to be started on the main thread currently. + nsCOMPtr<mozIStorageService> ss; + if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) { + NS_WARNING("Failed to get storage service!"); + } + } + + if (NS_FAILED(QuotaClient::Initialize())) { + NS_WARNING("Failed to initialize quota client!"); + } + + Preferences::RegisterCallbackAndCall(ShadowWritesPrefChangedCallback, + kShadowWritesPref); + + Preferences::RegisterCallbackAndCall(SnapshotPrefillPrefChangedCallback, + kSnapshotPrefillPref); + + Preferences::RegisterCallbackAndCall( + SnapshotGradualPrefillPrefChangedCallback, kSnapshotGradualPrefillPref); + + Preferences::RegisterCallbackAndCall(ClientValidationPrefChangedCallback, + kClientValidationPref); + +#ifdef DEBUG + gLocalStorageInitialized = true; +#endif +} + +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)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + PreparedDatastore* preparedDatastore = gPreparedDatastores->Get(aDatastoreId); + if (NS_WARN_IF(!preparedDatastore)) { + ASSERT_UNLESS_FUZZING(); + 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().take(); +} + +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; +} + +bool DeallocPBackgroundLSDatabaseParent(PBackgroundLSDatabaseParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<Database> actor = dont_AddRef(static_cast<Database*>(aActor)); + + return true; +} + +PBackgroundLSObserverParent* AllocPBackgroundLSObserverParent( + const uint64_t& aObserverId) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + if (NS_WARN_IF(!gPreparedObsevers)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + RefPtr<Observer> observer = gPreparedObsevers->Get(aObserverId); + if (NS_WARN_IF(!observer)) { + ASSERT_UNLESS_FUZZING(); + 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*>>* array; + if (!gObservers->Get(notNullObserver->Origin(), &array)) { + array = new nsTArray<NotNull<Observer*>>(); + gObservers->Put(notNullObserver->Origin(), array); + } + 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(QuotaClient::IsShuttingDownOnBackgroundThread())) { + return nullptr; + } + + Maybe<ContentParentId> contentParentId; + + uint64_t childID = BackgroundParent::GetChildID(aBackgroundActor); + if (childID) { + contentParentId = Some(ContentParentId(childID)); + } + + // If we're in the same process as the actor, we need to get the target event + // queue from the current RequestHelper. + nsCOMPtr<nsIEventTarget> mainEventTarget; + if (!BackgroundParent::IsOtherProcessActor(aBackgroundActor)) { + mainEventTarget = LSObject::GetSyncLoopEventTarget(); + } + + RefPtr<LSRequestBase> actor; + + switch (aParams.type()) { + case LSRequestParams::TLSRequestPreloadDatastoreParams: + case LSRequestParams::TLSRequestPrepareDatastoreParams: { + RefPtr<PrepareDatastoreOp> prepareDatastoreOp = + new PrepareDatastoreOp(mainEventTarget, 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(mainEventTarget, 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(!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(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; + } + + 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(!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; +} + +bool RecvLSClearPrivateBrowsing() { + AssertIsOnBackgroundThread(); + + gPrivateDatastores = nullptr; + + if (gDatastores) { + for (const auto& entry : *gDatastores) { + const auto& datastore = entry.GetData(); + MOZ_ASSERT(datastore); + + if (datastore->PrivateBrowsingId()) { + datastore->Clear(nullptr); + } + } + } + + 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.Data().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) { + AssertIsOnConnectionThread(); + 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) { + LS_TRY(PerformTruncate(aConnection, aShadowWrites)); + } + + for (auto iter = mWriteInfos.ConstIter(); !iter.Done(); iter.Next()) { + const WriteInfo* const writeInfo = iter.Data().get(); + + switch (writeInfo->GetType()) { + case WriteInfo::InsertItem: + case WriteInfo::UpdateItem: { + const auto* const insertItemInfo = + static_cast<const InsertItemInfo*>(writeInfo); + + LS_TRY(PerformInsertOrUpdate(aConnection, aShadowWrites, + insertItemInfo->GetKey(), + insertItemInfo->GetValue())); + + break; + } + + case WriteInfo::DeleteItem: { + const auto* const deleteItemInfo = + static_cast<const DeleteItemInfo*>(writeInfo); + + LS_TRY(PerformDelete(aConnection, aShadowWrites, + deleteItemInfo->GetKey())); + + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } + + { + Connection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement( + nsLiteralCString("UPDATE database " + "SET usage = usage + :delta"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + rv = stmt->BindInt64ByName("delta"_ns, mTotalDelta); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + } + + LS_TRY_INSPECT(const auto& stmt, CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *aConnection->StorageConnection(), + "SELECT usage FROM database"_ns)); + + LS_TRY(OkIf(stmt), Err(NS_ERROR_FAILURE)); + + LS_TRY_RETURN(MOZ_TO_RESULT_INVOKE(*stmt, GetInt64, 0)); +} + +nsresult ConnectionWriteOptimizer::PerformInsertOrUpdate( + Connection* aConnection, bool aShadowWrites, const nsAString& aKey, + const LSValue& aValue) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + Connection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement( + nsLiteralCString( + "INSERT OR REPLACE INTO data (key, value, utf16Length, compressed) " + "VALUES(:key, :value, :utf16Length, :compressed)"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName("key"_ns, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindUTF8StringByName("value"_ns, aValue); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName("utf16Length"_ns, aValue.UTF16Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName("compressed"_ns, aValue.IsCompressed()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aShadowWrites) { + return NS_OK; + } + + rv = aConnection->GetCachedStatement( + nsLiteralCString( + "INSERT OR REPLACE INTO shadow.webappsstore2 " + "(originAttributes, originKey, scope, key, value) " + "VALUES (:originAttributes, :originKey, :scope, :key, :value) "), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ArchivedOriginScope* archivedOriginScope = + aConnection->GetArchivedOriginScope(); + + rv = archivedOriginScope->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString scope = Scheme0Scope(archivedOriginScope->OriginSuffix(), + archivedOriginScope->OriginNoSuffix()); + + rv = stmt->BindUTF8StringByName("scope"_ns, scope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName("key"_ns, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aValue.IsCompressed()) { + nsCString value; + if (NS_WARN_IF(!SnappyUncompress(aValue, value))) { + return NS_ERROR_FAILURE; + } + rv = stmt->BindUTF8StringByName("value"_ns, value); + } else { + rv = stmt->BindUTF8StringByName("value"_ns, aValue); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult ConnectionWriteOptimizer::PerformDelete(Connection* aConnection, + bool aShadowWrites, + const nsAString& aKey) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + Connection::CachedStatement stmt; + nsresult rv = + aConnection->GetCachedStatement(nsLiteralCString("DELETE FROM data " + "WHERE key = :key;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName("key"_ns, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aShadowWrites) { + return NS_OK; + } + + rv = aConnection->GetCachedStatement( + nsLiteralCString("DELETE FROM shadow.webappsstore2 " + "WHERE originAttributes = :originAttributes " + "AND originKey = :originKey " + "AND key = :key;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindStringByName("key"_ns, aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult ConnectionWriteOptimizer::PerformTruncate(Connection* aConnection, + bool aShadowWrites) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + + Connection::CachedStatement stmt; + nsresult rv = aConnection->GetCachedStatement("DELETE FROM data;"_ns, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aShadowWrites) { + return NS_OK; + } + + rv = aConnection->GetCachedStatement( + nsLiteralCString("DELETE FROM shadow.webappsstore2 " + "WHERE originAttributes = :originAttributes " + "AND originKey = :originKey;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aConnection->GetArchivedOriginScope()->BindToStatement(stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +/******************************************************************************* + * 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() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(NS_SUCCEEDED(ResultCode())); + + if (!MayProceedOnNonOwningThread()) { + SetFailureCode(NS_ERROR_FAILURE); + } 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->StorageConnection()); + } + } + + 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_FAILURE); + } + + if (NS_SUCCEEDED(ResultCode())) { + OnSuccess(); + } else { + OnFailure(ResultCode()); + } + + Cleanup(); +} + +NS_IMETHODIMP +ConnectionDatastoreOperationBase::Run() { + if (IsOnConnectionThread()) { + RunOnConnectionThread(); + } else { + RunOnOwningThread(); + } + + return NS_OK; +} + +/******************************************************************************* + * Connection implementation + ******************************************************************************/ + +Connection::Connection(ConnectionThread* aConnectionThread, + const QuotaInfo& aQuotaInfo, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable) + : mConnectionThread(aConnectionThread), + mQuotaClient(QuotaClient::GetInstance()), + mArchivedOriginScope(std::move(aArchivedOriginScope)), + mQuotaInfo(aQuotaInfo), + mDatabaseWasNotAvailable(aDatabaseWasNotAvailable), + mHasCreatedDatabase(false), + mFlushScheduled(false) +#ifdef DEBUG + , + mInUpdateBatch(false), + mFinished(false) +#endif +{ + AssertIsOnOwningThread(); + MOZ_ASSERT(!aQuotaInfo.mGroup.IsEmpty()); + MOZ_ASSERT(!aQuotaInfo.mOrigin.IsEmpty()); +} + +Connection::~Connection() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mStorageConnection); + MOZ_ASSERT(!mCachedStatements.Count()); + 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() { + AssertIsOnConnectionThread(); + + if (mStorageConnection) { + return NS_OK; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + if (!mDatabaseWasNotAvailable || mHasCreatedDatabase) { + LS_TRY_INSPECT(const auto& directoryEntry, + quotaManager->GetDirectoryForOrigin(PERSISTENCE_TYPE_DEFAULT, + Origin())); + + LS_TRY(directoryEntry->Append( + NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))); + + LS_TRY(directoryEntry->GetPath(mDirectoryPath)); + LS_TRY(directoryEntry->Append(kDataFileName)); + + LS_TRY_INSPECT( + const auto& databaseFilePath, + MOZ_TO_RESULT_INVOKE_TYPED(nsString, directoryEntry, GetPath)); + + LS_TRY_UNWRAP(mStorageConnection, GetStorageConnection(databaseFilePath)); + + return NS_OK; + } + + RefPtr<InitTemporaryOriginHelper> helper = + new InitTemporaryOriginHelper(mQuotaInfo); + + LS_TRY_INSPECT(const auto& originDirectoryPath, + helper->BlockAndReturnOriginDirectoryPath()); + + LS_TRY_INSPECT(const auto& directoryEntry, + QM_NewLocalFile(originDirectoryPath)); + + LS_TRY(directoryEntry->Append( + NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))); + + LS_TRY(directoryEntry->GetPath(mDirectoryPath)); + + LS_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(directoryEntry, Exists)); + + if (!exists) { + LS_TRY(directoryEntry->Create(nsIFile::DIRECTORY_TYPE, 0755)); + } + + LS_TRY(directoryEntry->Append(kDataFileName)); + +#ifdef DEBUG + { + LS_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE(directoryEntry, Exists)); + + MOZ_ASSERT(!exists); + } +#endif + + LS_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_TARGET_DOES_NOT_EXIST && + rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + NS_WARNING("Failed to remove database file!"); + } + }); + + LS_TRY_UNWRAP(storageConnection, + CreateStorageConnection(*directoryEntry, *usageFile, Origin(), + [] { MOZ_ASSERT_UNREACHABLE(); })); + + MOZ_ASSERT(mQuotaClient); + + MutexAutoLock shadowDatabaseLock(mQuotaClient->ShadowDatabaseMutex()); + + nsCOMPtr<mozIStorageConnection> shadowConnection; + if (!gInitializedShadowStorage) { + LS_TRY_UNWRAP(shadowConnection, + CreateShadowStorageConnection(quotaManager->GetBasePath())); + + gInitializedShadowStorage = true; + } + + autoRemove.release(); + + if (!mHasCreatedDatabase) { + mHasCreatedDatabase = true; + } + + mStorageConnection = storageConnection; + + return NS_OK; +} + +void Connection::CloseStorageConnection() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + mCachedStatements.Clear(); + + MOZ_ALWAYS_SUCCEEDS(mStorageConnection->Close()); + mStorageConnection = nullptr; +} + +nsresult Connection::GetCachedStatement(const nsACString& aQuery, + CachedStatement* aCachedStatement) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(!aQuery.IsEmpty()); + MOZ_ASSERT(aCachedStatement); + MOZ_ASSERT(mStorageConnection); + + nsCOMPtr<mozIStorageStatement> stmt; + + if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { + nsresult rv = + mStorageConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { +#ifdef DEBUG + nsCString msg; + MOZ_ALWAYS_SUCCEEDS(mStorageConnection->GetLastErrorString(msg)); + + nsAutoCString error = "The statement '"_ns + aQuery + + "' failed to compile with the error message '"_ns + + msg + "'."_ns; + + NS_WARNING(error.get()); +#endif + return rv; + } + + mCachedStatements.Put(aQuery, stmt); + } + + aCachedStatement->Assign(this, stmt.forget()); + return NS_OK; +} + +nsresult Connection::BeginWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + CachedStatement stmt; + nsresult rv = GetCachedStatement("BEGIN IMMEDIATE;"_ns, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult Connection::CommitWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + CachedStatement stmt; + nsresult rv = GetCachedStatement("COMMIT;"_ns, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult Connection::RollbackWriteTransaction() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStorageConnection); + + CachedStatement stmt; + nsresult rv = GetCachedStatement("ROLLBACK;"_ns, &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // 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(); +} + +Connection::CachedStatement::CachedStatement() { + AssertIsOnConnectionThread(); + + MOZ_COUNT_CTOR(Connection::CachedStatement); +} + +Connection::CachedStatement::~CachedStatement() { + AssertIsOnConnectionThread(); + + MOZ_COUNT_DTOR(Connection::CachedStatement); +} + +Connection::CachedStatement::operator mozIStorageStatement*() const { + AssertIsOnConnectionThread(); + + return mStatement; +} + +mozIStorageStatement* Connection::CachedStatement::operator->() const { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mStatement); + + return mStatement; +} + +void Connection::CachedStatement::Assign( + Connection* aConnection, + already_AddRefed<mozIStorageStatement> aStatement) { + AssertIsOnConnectionThread(); + + mScoper.reset(); + + mStatement = aStatement; + + if (mStatement) { + mScoper.emplace(mStatement); + } +} + +Result<nsString, nsresult> +Connection::InitTemporaryOriginHelper::BlockAndReturnOriginDirectoryPath() { + AssertIsOnConnectionThread(); + + 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(); + } + + LS_TRY(mIOThreadResultCode); + + return mOriginDirectoryPath; +} + +nsresult Connection::InitTemporaryOriginHelper::RunOnIOThread() { + AssertIsOnIOThread(); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + LS_TRY_INSPECT(const auto& directoryEntry, + quotaManager + ->EnsureTemporaryOriginIsInitialized( + PERSISTENCE_TYPE_DEFAULT, mQuotaInfo) + .map([](const auto& res) { return res.first; })); + + LS_TRY(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() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + + AutoWriteTransaction autoWriteTransaction(mShadowWrites); + + LS_TRY(autoWriteTransaction.Start(mConnection)); + + LS_TRY_INSPECT(const int64_t& usage, + mWriteOptimizer.Perform(mConnection, mShadowWrites)); + + LS_TRY_INSPECT(const auto& usageFile, + GetUsageFile(mConnection->DirectoryPath())); + + LS_TRY_INSPECT(const auto& usageJournalFile, + GetUsageJournalFile(mConnection->DirectoryPath())); + + LS_TRY(UpdateUsageFile(usageFile, usageJournalFile, usage)); + + LS_TRY(autoWriteTransaction.Commit()); + + LS_TRY(usageJournalFile->Remove(false)); + + return NS_OK; +} + +void Connection::FlushOp::Cleanup() { + AssertIsOnOwningThread(); + + mWriteOptimizer.Reset(); + + MOZ_ASSERT(!mWriteOptimizer.HasWrites()); + + ConnectionDatastoreOperationBase::Cleanup(); +} + +nsresult Connection::CloseOp::DoDatastoreWork() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + + if (mConnection->StorageConnection()) { + 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 QuotaInfo& aQuotaInfo, + UniquePtr<ArchivedOriginScope>&& aArchivedOriginScope, + bool aDatabaseWasNotAvailable) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!aQuotaInfo.mOrigin.IsEmpty()); + MOZ_ASSERT(!mConnections.GetWeak(aQuotaInfo.mOrigin)); + + RefPtr<Connection> connection = + new Connection(this, aQuotaInfo, std::move(aArchivedOriginScope), + aDatabaseWasNotAvailable); + mConnections.Put(aQuotaInfo.mOrigin, RefPtr{connection}); + + return connection.forget(); +} + +void ConnectionThread::Shutdown() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mThread); + + mThread->Shutdown(); +} + +/******************************************************************************* + * Datastore + ******************************************************************************/ + +Datastore::Datastore(const GroupAndOrigin& aGroupAndOrigin, + uint32_t aPrivateBrowsingId, int64_t aUsage, + int64_t aSizeOfKeys, int64_t aSizeOfItems, + RefPtr<DirectoryLock>&& aDirectoryLock, + RefPtr<Connection>&& aConnection, + RefPtr<QuotaObject>&& aQuotaObject, + nsDataHashtable<nsStringHashKey, LSValue>& aValues, + nsTArray<LSItemInfo>&& aOrderedItems) + : mDirectoryLock(std::move(aDirectoryLock)), + mConnection(std::move(aConnection)), + mQuotaObject(std::move(aQuotaObject)), + mOrderedItems(std::move(aOrderedItems)), + mGroupAndOrigin(aGroupAndOrigin), + 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.GetEntry(aPrepareDatastoreOp)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPrepareDatastoreOps.PutEntry(aPrepareDatastoreOp); +} + +void Datastore::NoteFinishedPrepareDatastoreOp( + PrepareDatastoreOp* aPrepareDatastoreOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPrepareDatastoreOp); + MOZ_ASSERT(mPrepareDatastoreOps.GetEntry(aPrepareDatastoreOp)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPrepareDatastoreOps.RemoveEntry(aPrepareDatastoreOp); + + QuotaManager::GetRef().MaybeRecordShutdownStep( + 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::GetRef().MaybeRecordShutdownStep( + quota::Client::LS, "PrivateDatastore finished"_ns); + + MaybeClose(); +} + +void Datastore::NoteLivePreparedDatastore( + PreparedDatastore* aPreparedDatastore) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPreparedDatastore); + MOZ_ASSERT(!mPreparedDatastores.GetEntry(aPreparedDatastore)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPreparedDatastores.PutEntry(aPreparedDatastore); +} + +void Datastore::NoteFinishedPreparedDatastore( + PreparedDatastore* aPreparedDatastore) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPreparedDatastore); + MOZ_ASSERT(mPreparedDatastores.GetEntry(aPreparedDatastore)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mPreparedDatastores.RemoveEntry(aPreparedDatastore); + + QuotaManager::GetRef().MaybeRecordShutdownStep( + quota::Client::LS, "PreparedDatastore finished"_ns); + + MaybeClose(); +} + +void Datastore::NoteLiveDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(!mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mDatabases.PutEntry(aDatabase); +} + +void Datastore::NoteFinishedDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(mDirectoryLock); + MOZ_ASSERT(!mClosed); + + mDatabases.RemoveEntry(aDatabase); + + QuotaManager::GetRef().MaybeRecordShutdownStep(quota::Client::LS, + "Database finished"_ns); + + MaybeClose(); +} + +void Datastore::NoteActiveDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mActiveDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mClosed); + + mActiveDatabases.PutEntry(aDatabase); +} + +void Datastore::NoteInactiveDatabase(Database* aDatabase) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aDatabase); + MOZ_ASSERT(mDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(mActiveDatabases.GetEntry(aDatabase)); + MOZ_ASSERT(!mClosed); + + mActiveDatabases.RemoveEntry(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 nsString& 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 = [&](auto aKeyLength, auto 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 nsString& 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.Put(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 (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + const nsAString& key = iter.Key(); + const LSValue& value = iter.Data(); + + 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 aSnapshotInitialUsage) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSnapshotInitialUsage >= 0); + MOZ_ASSERT(!mClosed); + MOZ_ASSERT(mUpdateBatchUsage == -1); + MOZ_ASSERT(!mInUpdateBatch); + + mUpdateBatchUsage = aSnapshotInitialUsage; + + 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::RequestUpdateUsage(int64_t aRequestedSize, + int64_t aMinSize) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aRequestedSize > 0); + MOZ_ASSERT(aMinSize > 0); + + if (UpdateUsage(aRequestedSize)) { + return aRequestedSize; + } + + if (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(mGroupAndOrigin.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(mGroupAndOrigin.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 (auto iter = mActiveDatabases.ConstIter(); !iter.Done(); iter.Next()) { + Database* database = iter.Get()->GetKey(); + + 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(mGroupAndOrigin.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(PERSISTENCE_TYPE_DEFAULT, mGroupAndOrigin, + 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); + MOZ_ASSERT(gDatastores->Get(mGroupAndOrigin.mOrigin)); + gDatastores->Remove(mGroupAndOrigin.mOrigin); + + QuotaManager::GetRef().MaybeRecordShutdownStep(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 (auto iter = mDatabases.ConstIter(); !iter.Done(); iter.Next()) { + Database* database = iter.Get()->GetKey(); + + MOZ_ASSERT(database); + + if (database == aDatabase) { + continue; + } + + Snapshot* snapshot = database->GetSnapshot(); + if (snapshot) { + snapshot->SaveItem(aKey, aOldValue, aAffectsOrder); + } + } +} + +/******************************************************************************* + * 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::GetRef().MaybeRecordShutdownStep(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_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult Database::RecvAllowToClose() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mAllowedToClose)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + AllowToClose(); + + return IPC_OK(); +} + +PBackgroundLSSnapshotParent* Database::AllocPBackgroundLSSnapshotParent( + const nsString& aDocumentURI, const nsString& aKey, + const bool& aIncreasePeakUsage, const int64_t& aRequestedSize, + const int64_t& aMinSize, LSSnapshotInitInfo* aInitInfo) { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(aIncreasePeakUsage && aRequestedSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(aIncreasePeakUsage && aMinSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return nullptr; + } + + if (NS_WARN_IF(mAllowedToClose)) { + ASSERT_UNLESS_FUZZING(); + 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 nsString& aDocumentURI, + const nsString& aKey, const bool& aIncreasePeakUsage, + const int64_t& aRequestedSize, const int64_t& aMinSize, + LSSnapshotInitInfo* aInitInfo) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT_IF(aIncreasePeakUsage, aRequestedSize > 0); + 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); + + nsTHashtable<nsStringHashKey> unknownItems; + if (addKeyToUnknownItems) { + unknownItems.PutEntry(aKey); + } + + uint32_t totalLength = mDatastore->GetLength(); + + int64_t initialUsage = mDatastore->GetUsage(); + + int64_t peakUsage = initialUsage; + + if (aIncreasePeakUsage) { + int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize); + peakUsage += size; + } + + bool hasOtherProcessObservers = mDatastore->HasOtherProcessObservers(this); + + snapshot->Init(loadedItems, unknownItems, nextLoadIndex, totalLength, + initialUsage, peakUsage, loadState, hasOtherProcessObservers); + + RegisterSnapshot(snapshot); + + aInitInfo->addKeyToUnknownItems() = addKeyToUnknownItems; + aInitInfo->itemInfos() = std::move(itemInfos); + aInitInfo->totalLength() = totalLength; + aInitInfo->initialUsage() = initialUsage; + aInitInfo->peakUsage() = peakUsage; + aInitInfo->loadState() = loadState; + 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.GetEntry(aKey) && !mUnknownItems.GetEntry(aKey)) { + LSValue oldValue(aOldValue); + mValues.LookupForAdd(aKey).OrInsert([oldValue]() { return oldValue; }); + } + + 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_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvCheckpoint( + nsTArray<LSWriteInfo>&& aWriteInfos) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mUsage >= 0); + MOZ_ASSERT(mPeakUsage >= mUsage); + + if (NS_WARN_IF(aWriteInfos.IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mHasOtherProcessObservers)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + 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::RecvCheckpointAndNotify( + nsTArray<LSWriteAndNotifyInfo>&& aWriteAndNotifyInfos) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mUsage >= 0); + MOZ_ASSERT(mPeakUsage >= mUsage); + + if (NS_WARN_IF(aWriteAndNotifyInfos.IsEmpty())) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(!mHasOtherProcessObservers)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + 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::RecvFinish() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + Finish(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvLoaded() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedAllItems)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadKeysReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + mLoadedReceived = true; + + mLoadedItems.Clear(); + mUnknownItems.Clear(); + mValues.Clear(); + mKeys.Clear(); + mLoadedAllItems = true; + mLoadKeysReceived = true; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvLoadValueAndMoreItems( + const nsString& aKey, LSValue* aValue, nsTArray<LSItemInfo>* aItemInfos) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aValue); + MOZ_ASSERT(aItemInfos); + MOZ_ASSERT(mDatastore); + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedAllItems)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (mLoadedItems.GetEntry(aKey) || mUnknownItems.GetEntry(aKey)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (auto entry = mValues.Lookup(aKey)) { + *aValue = entry.Data(); + entry.Remove(); + } else { + mDatastore->GetItem(aKey, *aValue); + } + + if (aValue->IsVoid()) { + mUnknownItems.PutEntry(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 + for (auto iter = mValues.ConstIter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(iter.Data().IsVoid()); + } +#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)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadedReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mLoadKeysReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + mLoadKeysReceived = true; + + if (mSavedKeys) { + aKeys->AppendElements(std::move(mKeys)); + } else { + mDatastore->GetKeys(*aKeys); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvIncreasePeakUsage( + const int64_t& aRequestedSize, const int64_t& aMinSize, int64_t* aSize) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aSize); + + if (NS_WARN_IF(aRequestedSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(aMinSize <= 0)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + if (NS_WARN_IF(mFinishReceived)) { + ASSERT_UNLESS_FUZZING(); + return IPC_FAIL_NO_REASON(this); + } + + int64_t size = mDatastore->RequestUpdateUsage(aRequestedSize, aMinSize); + + mPeakUsage += size; + + *aSize = size; + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Snapshot::RecvPing() { + AssertIsOnBackgroundThread(); + + // Do nothing here. This is purely a sync message allowing the child to + // confirm that the actor has received previous async message. + + 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_NO_REASON(mgr); + } + return IPC_OK(); +} + +/******************************************************************************* + * LSRequestBase + ******************************************************************************/ + +LSRequestBase::LSRequestBase(nsIEventTarget* aMainEventTarget, + const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : mMainEventTarget(aMainEventTarget), + 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()))) { + 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()))) { + return false; + } + + if (NS_WARN_IF(!VerifyClientId(mContentParentId, + commonParams.principalInfo(), + 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()))) { + return false; + } + + if (NS_WARN_IF(!VerifyClientId(mContentParentId, params.principalInfo(), + 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_FAILURE; + } + +#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; + } + + LS_TRY(Start()); + + return NS_OK; +} + +void LSRequestBase::SendReadyMessage() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingReadyMessage); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + } + + 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_FAILURE; + } + + 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_FAILURE); + } + + 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_FAILURE)) { + return IPC_FAIL_NO_REASON(mgr); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult LSRequestBase::RecvFinish() { + AssertIsOnOwningThread(); + + Finish(); + + return IPC_OK(); +} + +/******************************************************************************* + * PrepareDatastoreOp + ******************************************************************************/ + +PrepareDatastoreOp::PrepareDatastoreOp( + nsIEventTarget* aMainEventTarget, const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : LSRequestBase(aMainEventTarget, aParams, aContentParentId), + mMainEventTarget(aMainEventTarget), + 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::QuotaManagerPending: + aResult.AppendLiteral("QuotaManagerPending"); + 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()); + + const LSRequestCommonParams& commonParams = + mForPreload + ? mParams.get_LSRequestPreloadDatastoreParams().commonParams() + : mParams.get_LSRequestPrepareDatastoreParams().commonParams(); + + const PrincipalInfo& storagePrincipalInfo = + commonParams.storagePrincipalInfo(); + + if (storagePrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + mQuotaInfo = QuotaManager::GetInfoForChrome(); + } else { + MOZ_ASSERT(storagePrincipalInfo.type() == + PrincipalInfo::TContentPrincipalInfo); + + QuotaInfo quotaInfo = + QuotaManager::GetInfoFromValidatedPrincipalInfo(storagePrincipalInfo); + + mQuotaInfo.mSuffix = std::move(quotaInfo.mSuffix); + mQuotaInfo.mGroup = std::move(quotaInfo.mGroup); + mMainThreadOrigin = std::move(quotaInfo.mOrigin); + } + + 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_FAILURE; + } + + 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. + mQuotaInfo.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; + } + } + + LS_TRY(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_FAILURE; + } + + LS_TRY(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; + } + + LS_TRY(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_FAILURE; + } + + LS_TRY(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()); + + if ((mDatastore = GetDatastore(Origin()))) { + MOZ_ASSERT(!mDatastore->IsClosed()); + + mDatastore->NoteLivePrepareDatastoreOp(this); + + FinishNesting(); + + return NS_OK; + } + + if (QuotaManager::Get()) { + nsresult rv = OpenDirectory(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + mNestedState = NestedState::QuotaManagerPending; + QuotaManager::GetOrCreate(this, mMainEventTarget); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::QuotaManagerOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::QuotaManagerPending); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + return NS_ERROR_FAILURE; + } + + LS_TRY(OkIf(QuotaManager::Get()), NS_ERROR_FAILURE); + + LS_TRY(OpenDirectory()); + + return NS_OK; +} + +nsresult PrepareDatastoreOp::OpenDirectory() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::Nesting); + MOZ_ASSERT(mNestedState == NestedState::PreparationPending || + mNestedState == NestedState::QuotaManagerPending); + MOZ_ASSERT(OriginIsKnown()); + MOZ_ASSERT(!mDirectoryLock); + MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); + MOZ_ASSERT(MayProceed()); + MOZ_ASSERT(QuotaManager::Get()); + + mNestedState = NestedState::DirectoryOpenPending; + mPendingDirectoryLock = QuotaManager::Get()->OpenDirectory( + PERSISTENCE_TYPE_DEFAULT, mQuotaInfo, mozilla::dom::quota::Client::LS, + /* aExclusive */ false, this); + + 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); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !MayProceedOnNonOwningThread()) { + return NS_ERROR_FAILURE; + } + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // This must be called before EnsureTemporaryStorageIsInitialized. + LS_TRY(quotaManager->EnsureStorageIsInitialized()); + + // This ensures that usages for existings origin directories are cached in + // memory. + LS_TRY(quotaManager->EnsureTemporaryStorageIsInitialized()); + + const UsageInfo usageInfo = quotaManager->GetUsageForClient( + PERSISTENCE_TYPE_DEFAULT, mQuotaInfo, mozilla::dom::quota::Client::LS); + + const bool hasUsage = usageInfo.DatabaseUsage().isSome(); + MOZ_ASSERT(usageInfo.FileUsage().isNothing()); + + if (!gArchivedOrigins) { + LS_TRY(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. + LS_TRY_INSPECT( + const auto& directoryEntry, + ([hasDataForMigration, "aManager, + this]() -> mozilla::Result<nsCOMPtr<nsIFile>, nsresult> { + if (hasDataForMigration) { + LS_TRY_RETURN(quotaManager + ->EnsureTemporaryOriginIsInitialized( + PERSISTENCE_TYPE_DEFAULT, mQuotaInfo) + .map([](const auto& res) { return res.first; })); + } + + LS_TRY_UNWRAP(auto directoryEntry, + quotaManager->GetDirectoryForOrigin( + PERSISTENCE_TYPE_DEFAULT, Origin())); + + quotaManager->EnsureQuotaForOrigin(PERSISTENCE_TYPE_DEFAULT, + mQuotaInfo); + + return directoryEntry; + }())); + + LS_TRY(directoryEntry->Append( + NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))); + + LS_TRY_INSPECT(const auto& directoryPath, + MOZ_TO_RESULT_INVOKE_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. + LS_TRY(EnsureDirectoryEntry(directoryEntry, + /* aCreateIfNotExists */ hasDataForMigration, + /* aIsDirectory */ true)); + + LS_TRY(directoryEntry->Append(kDataFileName)); + + LS_TRY(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; + LS_TRY(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(); + + LS_TRY(OkIf(quotaObject), Err(NS_ERROR_FAILURE)); + + LS_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath)); + + LS_TRY_INSPECT(const auto& usageJournalFile, + GetUsageJournalFile(directoryPath)); + + LS_TRY_INSPECT( + const auto& connection, + (CreateStorageConnection( + *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; + }))); + + LS_TRY(VerifyDatabaseInformation(connection)); + + if (hasDataForMigration) { + MOZ_ASSERT(mUsage == 0); + + LS_TRY(AttachArchiveDatabase(quotaManager->GetStoragePath(), connection)); + + LS_TRY_INSPECT(const int64_t& newUsage, + GetUsage(*connection, mArchivedOriginScope.get())); + + if (!quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) { + return 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); + + { + nsCOMPtr<mozIStorageFunction> function = new CompressFunction(); + + LS_TRY(connection->CreateFunction("compress"_ns, 1, function)); + + function = new CompressibleFunction(); + + LS_TRY(connection->CreateFunction("compressible"_ns, 1, function)); + + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "INSERT INTO data (key, value, utf16Length, compressed) " + "SELECT key, compress(value), utf16Length(value), " + "compressible(value) " + "FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;"_ns)); + + LS_TRY(mArchivedOriginScope->BindToStatement(stmt)); + + LS_TRY(stmt->Execute()); + + LS_TRY(connection->RemoveFunction("compress"_ns)); + + LS_TRY(connection->RemoveFunction("compressible"_ns)); + } + + { + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection, + CreateStatement, + "UPDATE database SET usage = :usage;"_ns)); + + LS_TRY(stmt->BindInt64ByName("usage"_ns, newUsage)); + + LS_TRY(stmt->Execute()); + } + + { + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, connection, CreateStatement, + "DELETE FROM webappsstore2 " + "WHERE originKey = :originKey " + "AND originAttributes = :originAttributes;"_ns)); + + LS_TRY(mArchivedOriginScope->BindToStatement(stmt)); + LS_TRY(stmt->Execute()); + } + + LS_TRY(UpdateUsageFile(usageFile, usageJournalFile, newUsage)); + LS_TRY(transaction.Commit()); + + autoUpdateSize.release(); + + LS_TRY(usageJournalFile->Remove(false)); + LS_TRY(DetachArchiveDatabase(connection)); + + MOZ_ASSERT(gArchivedOrigins); + MOZ_ASSERT(mArchivedOriginScope->HasMatches(gArchivedOrigins)); + mArchivedOriginScope->RemoveMatches(gArchivedOrigins); + + mUsage = newUsage; + } + + nsCOMPtr<mozIStorageConnection> shadowConnection; + if (!gInitializedShadowStorage) { + LS_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; + + LS_TRY(OwningEventTarget()->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +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); + + LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(aEntry, Exists)); + + if (!exists) { + if (!aCreateIfNotExists) { + if (aAlreadyExisted) { + *aAlreadyExisted = false; + } + return NS_OK; + } + + if (aIsDirectory) { + LS_TRY(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); + + LS_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *aConnection, "SELECT origin FROM database"_ns)); + + LS_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED); + + LS_TRY_INSPECT(const auto& origin, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0)); + + LS_TRY(OkIf(QuotaManager::AreOriginsEqualOnDisk(Origin(), origin)), + NS_ERROR_FILE_CORRUPTED); + + return NS_OK; +} + +already_AddRefed<QuotaObject> PrepareDatastoreOp::GetQuotaObject() { + MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread()); + MOZ_ASSERT(!mQuotaInfo.mGroup.IsEmpty()); + MOZ_ASSERT(OriginIsKnown()); + MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject( + PERSISTENCE_TYPE_DEFAULT, mQuotaInfo, mozilla::dom::quota::Client::LS, + mDatabaseFilePath, mUsage); + + if (!quotaObject) { + LS_WARNING("Failed to get quota object for group (%s) and origin (%s)!", + mQuotaInfo.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_FAILURE; + } + + if (!gConnectionThread) { + gConnectionThread = new ConnectionThread(); + } + + mConnection = gConnectionThread->CreateConnection( + mQuotaInfo, 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; + + LS_TRY(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::QuotaManagerPending: + rv = QuotaManagerOpen(); + 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( + mQuotaInfo, std::move(mArchivedOriginScope), + /* aDatabaseWasNotAvailable */ true); + MOZ_ASSERT(mConnection); + } + + quotaObject = GetQuotaObject(); + if (!quotaObject) { + aResponse = NS_ERROR_FAILURE; + return; + } + } + + mDatastore = new Datastore( + mQuotaInfo, 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->Get(Origin())); + gDatastores->Put(Origin(), mDatastore); + } + + if (mPrivateBrowsingId && !mInvalidated) { + if (!gPrivateDatastores) { + gPrivateDatastores = MakeUnique<PrivateDatastoreHashtable>(); + } + + if (!gPrivateDatastores->Get(Origin())) { + auto privateDatastore = + MakeUnique<PrivateDatastore>(WrapMovingNotNull(mDatastore)); + + gPrivateDatastores->Put(Origin(), std::move(privateDatastore)); + + mPrivateDatastoreRegistered.Flip(); + } + } + + mDatastoreId = ++gLastDatastoreId; + + auto preparedDatastore = MakeUnique<PreparedDatastore>( + mDatastore, mContentParentId, Origin(), mDatastoreId, + /* aForPreload */ mForPreload); + + if (!gPreparedDatastores) { + gPreparedDatastores = new PreparedDatastoreHashtable(); + } + gPreparedDatastores->Put(mDatastoreId, preparedDatastore.get()); + + if (mInvalidated) { + preparedDatastore->Invalidate(); + } + + mPreparedDatastoreRegistered.Flip(); + + Unused << preparedDatastore.release(); + + 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::GetRef().MaybeRecordShutdownStep( + quota::Client::LS, "PrepareDatastoreOp completed"_ns); + + if (gPrepareDatastoreOps->IsEmpty()) { + gPrepareDatastoreOps = nullptr; + } +} + +NS_IMPL_ISUPPORTS_INHERITED0(PrepareDatastoreOp, LSRequestBase) + +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_FAILURE); + + 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() { + AssertIsOnConnectionThread(); + 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_FAILURE; + } + + Connection::CachedStatement stmt; + nsresult rv = mConnection->GetCachedStatement( + nsLiteralCString("SELECT key, value, utf16Length, compressed " + "FROM data;"), + &stmt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LS_TRY(quota::CollectWhileHasResult( + *stmt, [this](auto& stmt) -> mozilla::Result<Ok, nsresult> { + LS_TRY_UNWRAP(auto key, + MOZ_TO_RESULT_INVOKE_TYPED(nsString, stmt, GetString, 0)); + + LSValue value; + LS_TRY(value.InitFromStatement(&stmt, 1)); + + mPrepareDatastoreOp->mValues.Put(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 + + LS_TRY_INSPECT(const auto& value, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, aFunctionArguments, + GetUTF8String, 0)); + + nsCString compressed; + LS_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_FAILURE); + + nsCOMPtr<nsIVariant> result = + new storage::UTF8TextVariant(compressed.IsVoid() ? value : compressed); + + result.forget(aResult); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(PrepareDatastoreOp::CompressibleFunction, mozIStorageFunction) + +NS_IMETHODIMP +PrepareDatastoreOp::CompressibleFunction::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 + + LS_TRY_INSPECT(const auto& value, + MOZ_TO_RESULT_INVOKE_TYPED(nsCString, aFunctionArguments, + GetUTF8String, 0)); + + nsCString compressed; + LS_TRY(OkIf(SnappyCompress(value, compressed)), NS_ERROR_FAILURE); + + const bool compressible = !compressed.IsVoid(); + + nsCOMPtr<nsIVariant> result = new storage::IntegerVariant(compressible); + + result.forget(aResult); + return NS_OK; +} + +/******************************************************************************* + * PrepareObserverOp + ******************************************************************************/ + +PrepareObserverOp::PrepareObserverOp( + nsIEventTarget* aMainEventTarget, const LSRequestParams& aParams, + const Maybe<ContentParentId>& aContentParentId) + : LSRequestBase(aMainEventTarget, 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->Put(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()))) { + 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_FAILURE; + } + +#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; + } + + LS_TRY(Start()); + + return NS_OK; +} + +void LSSimpleRequestBase::SendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnBackgroundThread()) || + !MayProceed()) { + MaybeSetFailureCode(NS_ERROR_FAILURE); + } + + 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; +} + +/******************************************************************************* + * 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() || IsOnConnectionThread()); + MOZ_ASSERT(aStmt); + + struct Matcher { + mozIStorageStatement* mStmt; + + explicit Matcher(mozIStorageStatement* aStmt) : mStmt(aStmt) {} + + nsresult operator()(const Origin& aOrigin) { + LS_TRY(mStmt->BindUTF8StringByName("originKey"_ns, + aOrigin.OriginNoSuffix())); + + LS_TRY(mStmt->BindUTF8StringByName("originAttributes"_ns, + aOrigin.OriginSuffix())); + + return NS_OK; + } + + nsresult operator()(const Prefix& aPrefix) { + LS_TRY(mStmt->BindUTF8StringByName("originKey"_ns, + aPrefix.OriginNoSuffix())); + + return NS_OK; + } + + nsresult operator()(const Pattern& aPattern) { + LS_TRY(mStmt->BindUTF8StringByName("originAttributesPattern"_ns, + "pattern1"_ns)); + + return NS_OK; + } + + nsresult operator()(const Null& aNull) { return NS_OK; } + }; + + LS_TRY(mData.match(Matcher(aStmt))); + + return NS_OK; +} + +bool ArchivedOriginScope::HasMatches( + ArchivedOriginHashtable* aHashtable) const { + AssertIsOnIOThread(); + MOZ_ASSERT(aHashtable); + + struct Matcher { + ArchivedOriginHashtable* mHashtable; + + explicit Matcher(ArchivedOriginHashtable* aHashtable) + : mHashtable(aHashtable) {} + + bool operator()(const Origin& aOrigin) { + nsCString hashKey = GetArchivedOriginHashKey(aOrigin.OriginSuffix(), + aOrigin.OriginNoSuffix()); + + ArchivedOriginInfo* archivedOriginInfo; + return mHashtable->Get(hashKey, &archivedOriginInfo); + } + + bool operator()(const Prefix& aPrefix) { + for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) { + const auto& archivedOriginInfo = iter.Data(); + + if (archivedOriginInfo->mOriginNoSuffix == aPrefix.OriginNoSuffix()) { + return true; + } + } + + return false; + } + + bool operator()(const Pattern& aPattern) { + for (auto iter = mHashtable->ConstIter(); !iter.Done(); iter.Next()) { + const auto& archivedOriginInfo = iter.Data(); + + if (aPattern.GetPattern().Matches( + archivedOriginInfo->mOriginAttributes)) { + return true; + } + } + + return false; + } + + bool operator()(const Null& aNull) { return mHashtable->Count(); } + }; + + return mData.match(Matcher(aHashtable)); +} + +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"), + mShutdownRequested(false) { + 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; +} + +// static +nsresult QuotaClient::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = Observer::Initialize(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +mozilla::dom::quota::Client::Type QuotaClient::GetType() { + return QuotaClient::LS; +} + +Result<UsageInfo, nsresult> QuotaClient::InitOrigin( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + LS_TRY_INSPECT(const auto& directory, + quotaManager->GetDirectoryForOrigin(aPersistenceType, + aGroupAndOrigin.mOrigin)); + + MOZ_ASSERT(directory); + + LS_TRY(directory->Append(NS_LITERAL_STRING_FROM_CSTRING(LS_DIRECTORY_NAME))); + +#ifdef DEBUG + { + LS_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE(directory, Exists)); + MOZ_ASSERT(exists); + } +#endif + + LS_TRY_INSPECT(const auto& directoryPath, + MOZ_TO_RESULT_INVOKE_TYPED(nsString, directory, GetPath)); + + LS_TRY_INSPECT(const auto& usageFile, GetUsageFile(directoryPath)); + + // XXX Try to make usageFileExists const + LS_TRY_UNWRAP(bool usageFileExists, ExistsAsFile(*usageFile)); + + LS_TRY_INSPECT(const auto& usageJournalFile, + GetUsageJournalFile(directoryPath)); + + LS_TRY_INSPECT(const bool& usageJournalFileExists, + ExistsAsFile(*usageJournalFile)); + + if (usageJournalFileExists) { + if (usageFileExists) { + LS_TRY(usageFile->Remove(false)); + + usageFileExists = false; + } + + LS_TRY(usageJournalFile->Remove(false)); + } + + LS_TRY_INSPECT(const auto& file, + CloneFileAndAppend(*directory, kDataFileName)); + + LS_TRY_INSPECT(const bool& fileExists, ExistsAsFile(*file)); + + LS_TRY_INSPECT( + const UsageInfo& res, + ([fileExists, usageFileExists, &file, &usageFile, &usageJournalFile, + &aGroupAndOrigin]() -> Result<UsageInfo, nsresult> { + if (fileExists) { + LS_TRY_RETURN( + // To simplify control flow, we call LoadUsageFile unconditionally + // here, even though it will necessarily fail if usageFileExists + // is false. + LoadUsageFile(*usageFile) + .orElse([&file, &usageFile, &usageJournalFile, + &aGroupAndOrigin]( + const nsresult) -> Result<UsageInfo, nsresult> { + LS_TRY_INSPECT( + const auto& connection, + CreateStorageConnection( + *file, *usageFile, aGroupAndOrigin.mOrigin, [] {})); + + LS_TRY_INSPECT( + const int64_t& usage, + GetUsage(*connection, + /* aArchivedOriginScope */ nullptr)); + + LS_TRY(UpdateUsageFile(usageFile, usageJournalFile, usage)); + + LS_TRY(usageJournalFile->Remove(false)); + + MOZ_ASSERT(usage >= 0); + return UsageInfo{DatabaseUsageType(Some(uint64_t(usage)))}; + })); + } + + if (usageFileExists) { + LS_TRY(usageFile->Remove(false)); + } + + return UsageInfo{}; + }())); + + // Report unknown files in debug builds, but don't fail, just warn. + +#ifdef DEBUG + LS_TRY(CollectEachFileAtomicCancelable( + *directory, aCanceled, + [](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + LS_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE(file, IsDirectory)); + + if (isDirectory) { + Unused << WARN_IF_FILE_IS_UNKNOWN(*file); + return Ok{}; + } + + LS_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_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); + + return Ok{}; + })); +#endif + + return res; +} + +nsresult QuotaClient::InitOriginWithoutTracking( + PersistenceType aPersistenceType, const GroupAndOrigin& aGroupAndOrigin, + const AtomicBool& aCanceled) { + AssertIsOnIOThread(); + + // This is called when a storage/permanent/chrome/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 GroupAndOrigin& aGroupAndOrigin, + 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, + aGroupAndOrigin, 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; + } + + const bool shadowWrites = gShadowWrites; + + LS_TRY_INSPECT(const auto& archivedOriginScope, + CreateArchivedOriginScope(aOriginScope)); + + if (!gArchivedOrigins) { + LS_TRY(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); + + LS_TRY_INSPECT( + const auto& connection, + ([&basePath]() -> Result<nsCOMPtr<mozIStorageConnection>, nsresult> { + if (gInitializedShadowStorage) { + LS_TRY_RETURN(GetShadowStorageConnection(basePath)); + } + + LS_TRY_UNWRAP(auto connection, + CreateShadowStorageConnection(basePath)); + + gInitializedShadowStorage = true; + + return connection; + }())); + + if (hasDataForRemoval) { + LS_TRY(AttachArchiveDatabase(quotaManager->GetStoragePath(), connection)); + } + + if (archivedOriginScope->IsPattern()) { + nsCOMPtr<mozIStorageFunction> function( + new MatchFunction(archivedOriginScope->GetPattern())); + + LS_TRY(connection->CreateFunction("match"_ns, 2, function)); + } + + { + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection, + CreateStatement, "BEGIN IMMEDIATE;"_ns)); + + LS_TRY(stmt->Execute()); + } + + if (shadowWrites) { + LS_TRY(PerformDelete(connection, "main"_ns, archivedOriginScope.get())); + } + + if (hasDataForRemoval) { + LS_TRY( + PerformDelete(connection, "archive"_ns, archivedOriginScope.get())); + } + + { + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, connection, + CreateStatement, "COMMIT;"_ns)); + + LS_TRY(stmt->Execute()); + } + + if (archivedOriginScope->IsPattern()) { + LS_TRY(connection->RemoveFunction("match"_ns)); + } + + if (hasDataForRemoval) { + LS_TRY(DetachArchiveDatabase(connection)); + + MOZ_ASSERT(gArchivedOrigins); + MOZ_ASSERT(archivedOriginScope->HasMatches(gArchivedOrigins)); + archivedOriginScope->RemoveMatches(gArchivedOrigins); + } + + LS_TRY(connection->Close()); + } + + if (aOriginScope.IsNull()) { + LS_TRY_INSPECT(const auto& shadowFile, GetShadowFile(basePath)); + + LS_TRY(shadowFile->Remove(false)); + + gInitializedShadowStorage = false; + } + + return NS_OK; +} + +void QuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + AssertIsOnIOThread(); +} + +void QuotaClient::ReleaseIOThreadObjects() { + AssertIsOnIOThread(); + + // 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() { + MOZ_ASSERT(!mShutdownRequested); + mShutdownRequested = true; + + // 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 (gPrivateDatastores) { + gPrivateDatastores->Clear(); + gPrivateDatastores = nullptr; + } + + if (gPreparedDatastores) { + gPreparedDatastores->Clear(); + gPreparedDatastores = nullptr; + } + + RequestAllowToCloseDatabasesMatching([](const auto&) { return true; }); + + if (gPreparedObsevers) { + gPreparedObsevers->Clear(); + 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(" ("); + + nsTHashtable<nsCStringHashKey> ids; + + for (const auto& prepareDatastoreOp : *gPrepareDatastoreOps) { + nsCString id; + prepareDatastoreOp->Stringify(id); + + ids.PutEntry(id); + } + + StringifyTableKeys(ids, data); + + data.Append(")\n"); + } + + if (gDatastores) { + data.Append("Datastores: "); + data.AppendInt(gDatastores->Count()); + data.Append(" ("); + + nsTHashtable<nsCStringHashKey> ids; + + for (const auto& entry : *gDatastores) { + MOZ_ASSERT(entry.GetData()); + + nsCString id; + entry.GetData()->Stringify(id); + + ids.PutEntry(id); + } + + StringifyTableKeys(ids, data); + + data.Append(")\n"); + } + + if (gLiveDatabases) { + data.Append("LiveDatabases: "); + data.AppendInt(static_cast<uint32_t>(gLiveDatabases->Length())); + data.Append(" ("); + + nsTHashtable<nsCStringHashKey> ids; + + for (const auto& database : *gLiveDatabases) { + nsCString id; + database->Stringify(id); + + ids.PutEntry(id); + } + + StringifyTableKeys(ids, data); + + 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()) { + LS_TRY_INSPECT(const auto& principalInfo, + QuotaManager::ParseOrigin(aOriginScope.GetOrigin())); + + LS_TRY_INSPECT((const auto& [originAttrSuffix, originKey]), + GenerateOriginKey2(principalInfo)); + + return ArchivedOriginScope::CreateFromOrigin(originAttrSuffix, originKey); + } + + if (aOriginScope.IsPrefix()) { + LS_TRY_INSPECT(const auto& principalInfo, + QuotaManager::ParseOrigin(aOriginScope.GetOriginNoSuffix())); + + LS_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); + + LS_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "DELETE FROM "_ns + aSchemaName + ".webappsstore2"_ns + + aArchivedOriginScope->GetBindingClause() + ";"_ns)); + + LS_TRY(aArchivedOriginScope->BindToStatement(stmt)); + + LS_TRY(stmt->Execute()); + + return NS_OK; +} + +// static +nsresult QuotaClient::Observer::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Observer> observer = new Observer(); + + LS_TRY(observer->Init()); + + return NS_OK; +} + +nsresult QuotaClient::Observer::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = obs->AddObserver(this, kPrivateBrowsingObserverTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + return NS_OK; +} + +nsresult QuotaClient::Observer::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kPrivateBrowsingObserverTopic)); + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID)); + + // In general, the instance will have died after the latter removal call, so + // it's not safe to do anything after that point. + // However, Shutdown is currently called from Observe which is called by the + // Observer Service which holds a strong reference to the observer while the + // Observe method is being called. + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(QuotaClient::Observer, nsIObserver) + +NS_IMETHODIMP +QuotaClient::Observer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) { + PBackgroundChild* const backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + LS_TRY(OkIf(backgroundActor), NS_ERROR_FAILURE); + + LS_TRY(OkIf(backgroundActor->SendLSClearPrivateBrowsing()), + NS_ERROR_FAILURE); + + return NS_OK; + } + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + LS_TRY(Shutdown()); + + return NS_OK; + } + + NS_WARNING("Unknown observer topic!"); + 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); + + LS_TRY_INSPECT(const auto& suffix, + MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, aFunctionArguments, + GetUTF8String, 1)); + + OriginAttributes oa; + LS_TRY(OkIf(oa.PopulateFromSuffix(suffix)), NS_ERROR_FAILURE); + + const bool result = mPattern.Matches(oa); + + RefPtr<nsVariant> outVar(new nsVariant()); + LS_TRY(outVar->SetAsBool(result)); + + outVar.forget(aResult); + return NS_OK; +} + +/******************************************************************************* + * AutoWriteTransaction + ******************************************************************************/ + +AutoWriteTransaction::AutoWriteTransaction(bool aShadowWrites) + : mConnection(nullptr), mShadowWrites(aShadowWrites) { + AssertIsOnConnectionThread(); + + MOZ_COUNT_CTOR(mozilla::dom::AutoWriteTransaction); +} + +AutoWriteTransaction::~AutoWriteTransaction() { + AssertIsOnConnectionThread(); + + MOZ_COUNT_DTOR(mozilla::dom::AutoWriteTransaction); + + if (mConnection) { + if (NS_FAILED(mConnection->RollbackWriteTransaction())) { + NS_WARNING("Failed to rollback write transaction!"); + } + + if (mShadowWrites && NS_FAILED(DetachShadowDatabaseAndUnlock())) { + NS_WARNING("Failed to detach shadow database!"); + } + } +} + +nsresult AutoWriteTransaction::Start(Connection* aConnection) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!mConnection); + + if (mShadowWrites) { + LS_TRY(LockAndAttachShadowDatabase(aConnection)); + } + + LS_TRY(aConnection->BeginWriteTransaction()); + + mConnection = aConnection; + + return NS_OK; +} + +nsresult AutoWriteTransaction::Commit() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + + LS_TRY(mConnection->CommitWriteTransaction()); + + if (mShadowWrites) { + LS_TRY(DetachShadowDatabaseAndUnlock()); + } + + mConnection = nullptr; + + return NS_OK; +} + +nsresult AutoWriteTransaction::LockAndAttachShadowDatabase( + Connection* aConnection) { + AssertIsOnConnectionThread(); + MOZ_ASSERT(aConnection); + MOZ_ASSERT(!mConnection); + MOZ_ASSERT(mShadowDatabaseLock.isNothing()); + MOZ_ASSERT(mShadowWrites); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + nsCOMPtr<mozIStorageConnection> storageConnection = + aConnection->StorageConnection(); + MOZ_ASSERT(storageConnection); + + mShadowDatabaseLock.emplace( + aConnection->GetQuotaClient()->ShadowDatabaseMutex()); + + LS_TRY(AttachShadowDatabase(quotaManager->GetBasePath(), storageConnection)); + + return NS_OK; +} + +nsresult AutoWriteTransaction::DetachShadowDatabaseAndUnlock() { + AssertIsOnConnectionThread(); + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mShadowDatabaseLock.isSome()); + MOZ_ASSERT(mShadowWrites); + + nsCOMPtr<mozIStorageConnection> storageConnection = + mConnection->StorageConnection(); + MOZ_ASSERT(storageConnection); + + LS_TRY(DetachShadowDatabase(storageConnection)); + + mShadowDatabaseLock.reset(); + + return NS_OK; +} + +} // namespace mozilla::dom |