diff options
Diffstat (limited to 'dom/quota/ActorsParent.cpp')
-rw-r--r-- | dom/quota/ActorsParent.cpp | 11260 |
1 files changed, 11260 insertions, 0 deletions
diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp new file mode 100644 index 0000000000..eb0f8706fd --- /dev/null +++ b/dom/quota/ActorsParent.cpp @@ -0,0 +1,11260 @@ +/* -*- 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 "CanonicalQuotaObject.h" +#include "ClientUsageArray.h" +#include "Flatten.h" +#include "FirstInitializationAttemptsImpl.h" +#include "GroupInfo.h" +#include "GroupInfoPair.h" +#include "OriginScope.h" +#include "OriginInfo.h" +#include "QuotaCommon.h" +#include "QuotaManager.h" +#include "ScopedLogExtraInfo.h" +#include "UsageInfo.h" + +// Global includes +#include <cinttypes> +#include <cstdlib> +#include <cstring> +#include <algorithm> +#include <cstdint> +#include <functional> +#include <new> +#include <numeric> +#include <tuple> +#include <type_traits> +#include <utility> +#include "DirectoryLockImpl.h" +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageConnection.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/CondVar.h" +#include "mozilla/InitializedOnce.h" +#include "mozilla/Logging.h" +#include "mozilla/MacroForEach.h" +#include "mozilla/Maybe.h" +#include "mozilla/Mutex.h" +#include "mozilla/NotNull.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/SystemPrincipal.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "mozilla/TextUtils.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/FileSystemQuotaClient.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/LocalStorageCommon.h" +#include "mozilla/dom/StorageDBUpdater.h" +#include "mozilla/dom/cache/QuotaClient.h" +#include "mozilla/dom/indexedDB/ActorsParent.h" +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/dom/localstorage/ActorsParent.h" +#include "mozilla/dom/quota/AssertionsImpl.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/Constants.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/PQuota.h" +#include "mozilla/dom/quota/PQuotaParent.h" +#include "mozilla/dom/quota/PQuotaRequest.h" +#include "mozilla/dom/quota/PQuotaRequestParent.h" +#include "mozilla/dom/quota/PQuotaUsageRequest.h" +#include "mozilla/dom/quota/PQuotaUsageRequestParent.h" +#include "mozilla/dom/quota/QuotaManagerService.h" +#include "mozilla/dom/quota/QuotaManagerImpl.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" +#include "mozilla/dom/simpledb/ActorsParent.h" +#include "mozilla/fallible.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/net/ExtensionProtocolHandler.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsClassHashtable.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsTHashMap.h" +#include "nsDebug.h" +#include "nsDirectoryServiceUtils.h" +#include "nsError.h" +#include "nsHashKeys.h" +#include "nsIBinaryInputStream.h" +#include "nsIBinaryOutputStream.h" +#include "nsIConsoleService.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIDUtils.h" +#include "nsIEventTarget.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIInputStream.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIQuotaRequests.h" +#include "nsIPlatformInfo.h" +#include "nsIPrincipal.h" +#include "nsIRunnable.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsISupports.h" +#include "nsISupportsPrimitives.h" +#include "nsIThread.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsLiteralString.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsStandardURL.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsStringFlags.h" +#include "nsStringFwd.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsTLiteralString.h" +#include "nsTPromiseFlatString.h" +#include "nsTStringRepr.h" +#include "nsThreadUtils.h" +#include "nsURLHelper.h" +#include "nsXPCOM.h" +#include "nsXPCOMCID.h" +#include "nsXULAppAPI.h" +#include "prinrval.h" +#include "prio.h" +#include "prthread.h" +#include "prtime.h" + +// The amount of time, in milliseconds, that our IO thread will stay alive +// after the last event it processes. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +/** + * If shutdown takes this long, kill actors of a quota client, to avoid reaching + * the crash timeout. + */ +#define SHUTDOWN_KILL_ACTORS_TIMEOUT_MS 5000 + +/** + * Automatically crash the browser if shutdown of a quota client takes this + * long. We've chosen a value that is long enough that it is unlikely for the + * problem to be falsely triggered by slow system I/O. We've also chosen a + * value long enough so that automated tests should time out and fail if + * shutdown of a quota client takes too long. Also, this value is long enough + * so that testers can notice the timeout; we want to know about the timeouts, + * not hide them. On the other hand this value is less than 60 seconds which is + * used by nsTerminator to crash a hung main process. + */ +#define SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS 45000 + +static_assert( + SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS > SHUTDOWN_KILL_ACTORS_TIMEOUT_MS, + "The kill actors timeout must be shorter than the crash browser one."); + +// profile-before-change, when we need to shut down quota manager +#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm" + +#define KB *1024ULL +#define MB *1024ULL KB +#define GB *1024ULL MB + +namespace mozilla::dom::quota { + +using namespace mozilla::ipc; + +// We want profiles to be platform-independent so we always need to replace +// the same characters on every platform. Windows has the most extensive set +// of illegal characters so we use its FILE_ILLEGAL_CHARACTERS and +// FILE_PATH_SEPARATOR. +const char QuotaManager::kReplaceChars[] = CONTROL_CHARACTERS "/:*?\"<>|\\"; +const char16_t QuotaManager::kReplaceChars16[] = + u"" CONTROL_CHARACTERS "/:*?\"<>|\\"; + +namespace { + +/******************************************************************************* + * Constants + ******************************************************************************/ + +const uint32_t kSQLitePageSizeOverride = 512; + +// Important version history: +// - Bug 1290481 bumped our schema from major.minor 2.0 to 3.0 in Firefox 57 +// which caused Firefox 57 release concerns because the major schema upgrade +// means anyone downgrading to Firefox 56 will experience a non-operational +// QuotaManager and all of its clients. +// - Bug 1404344 got very concerned about that and so we decided to effectively +// rename 3.0 to 2.1, effective in Firefox 57. This works because post +// storage.sqlite v1.0, QuotaManager doesn't care about minor storage version +// increases. It also works because all the upgrade did was give the DOM +// Cache API QuotaClient an opportunity to create its newly added .padding +// files during initialization/upgrade, which isn't functionally necessary as +// that can be done on demand. + +// Major storage version. Bump for backwards-incompatible changes. +// (The next major version should be 4 to distinguish from the Bug 1290481 +// downgrade snafu.) +const uint32_t kMajorStorageVersion = 2; + +// Minor storage version. Bump for backwards-compatible changes. +const uint32_t kMinorStorageVersion = 3; + +// The storage version we store in the SQLite database is a (signed) 32-bit +// integer. The major version is left-shifted 16 bits so the max value is +// 0xFFFF. The minor version occupies the lower 16 bits and its max is 0xFFFF. +static_assert(kMajorStorageVersion <= 0xFFFF, + "Major version needs to fit in 16 bits."); +static_assert(kMinorStorageVersion <= 0xFFFF, + "Minor version needs to fit in 16 bits."); + +const int32_t kStorageVersion = + int32_t((kMajorStorageVersion << 16) + kMinorStorageVersion); + +// See comments above about why these are a thing. +const int32_t kHackyPreDowngradeStorageVersion = int32_t((3 << 16) + 0); +const int32_t kHackyPostDowngradeStorageVersion = int32_t((2 << 16) + 1); + +const char kChromeOrigin[] = "chrome"; +const char kAboutHomeOriginPrefix[] = "moz-safe-about:home"; +const char kIndexedDBOriginPrefix[] = "indexeddb://"; +const char kResourceOriginPrefix[] = "resource://"; + +constexpr auto kStorageName = u"storage"_ns; +constexpr auto kSQLiteSuffix = u".sqlite"_ns; + +#define INDEXEDDB_DIRECTORY_NAME u"indexedDB" +#define ARCHIVES_DIRECTORY_NAME u"archives" +#define PERSISTENT_DIRECTORY_NAME u"persistent" +#define PERMANENT_DIRECTORY_NAME u"permanent" +#define TEMPORARY_DIRECTORY_NAME u"temporary" +#define DEFAULT_DIRECTORY_NAME u"default" +#define DEFAULT_PRIVATE_DIRECTORY_NAME u"private" + +// The name of the file that we use to load/save the last access time of an +// origin. +// XXX We should get rid of old metadata files at some point, bug 1343576. +#define METADATA_FILE_NAME u".metadata" +#define METADATA_TMP_FILE_NAME u".metadata-tmp" +#define METADATA_V2_FILE_NAME u".metadata-v2" +#define METADATA_V2_TMP_FILE_NAME u".metadata-v2-tmp" + +#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite" +#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite" +#define LS_ARCHIVE_TMP_FILE_NAME u"ls-archive-tmp.sqlite" + +const int32_t kLocalStorageArchiveVersion = 4; + +const char kProfileDoChangeTopic[] = "profile-do-change"; +const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited"; + +const int32_t kCacheVersion = 2; + +/****************************************************************************** + * SQLite functions + ******************************************************************************/ + +int32_t MakeStorageVersion(uint32_t aMajorStorageVersion, + uint32_t aMinorStorageVersion) { + return int32_t((aMajorStorageVersion << 16) + aMinorStorageVersion); +} + +uint32_t GetMajorStorageVersion(int32_t aStorageVersion) { + return uint32_t(aStorageVersion >> 16); +} + +nsresult CreateTables(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // Table `database` + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("CREATE TABLE database" + "( cache_version INTEGER NOT NULL DEFAULT 0" + ");"_ns))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == 0); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kStorageVersion))); + + return NS_OK; +} + +Result<int32_t, nsresult> LoadCacheVersion(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT cache_version FROM database"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); +} + +nsresult SaveCacheVersion(mozIStorageConnection& aConnection, + int32_t aVersion) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "UPDATE database SET cache_version = :version;"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, aVersion))); + + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; +} + +nsresult CreateCacheTables(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + // Table `cache` + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL("CREATE TABLE cache" + "( valid INTEGER NOT NULL DEFAULT 0" + ", build_id TEXT NOT NULL DEFAULT ''" + ");"_ns))); + + // Table `repository` + QM_TRY( + MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("CREATE TABLE repository" + "( id INTEGER PRIMARY KEY" + ", name TEXT NOT NULL" + ");"_ns))); + + // Table `origin` + QM_TRY(MOZ_TO_RESULT( + aConnection.ExecuteSimpleSQL("CREATE TABLE origin" + "( repository_id INTEGER NOT NULL" + ", suffix TEXT" + ", group_ TEXT NOT NULL" + ", origin TEXT NOT NULL" + ", client_usages TEXT NOT NULL" + ", usage INTEGER NOT NULL" + ", last_access_time INTEGER NOT NULL" + ", accessed INTEGER NOT NULL" + ", persisted INTEGER NOT NULL" + ", PRIMARY KEY (repository_id, origin)" + ", FOREIGN KEY (repository_id) " + "REFERENCES repository(id) " + ");"_ns))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection)); + MOZ_ASSERT(cacheVersion == 0); + } +#endif + + QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, kCacheVersion))); + + return NS_OK; +} + +OkOrErr InvalidateCache(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + static constexpr auto kDeleteCacheQuery = "DELETE FROM origin;"_ns; + static constexpr auto kSetInvalidFlagQuery = "UPDATE cache SET valid = 0"_ns; + + QM_TRY(QM_OR_ELSE_WARN( + // Expression. + ([&]() -> OkOrErr { + mozStorageTransaction transaction(&aConnection, + /*aCommitOnComplete */ false); + + QM_TRY(QM_TO_RESULT(transaction.Start())); + QM_TRY(QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kDeleteCacheQuery))); + QM_TRY( + QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery))); + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return Ok{}; + }()), + // Fallback. + ([&](const QMResult& rv) -> OkOrErr { + QM_TRY( + QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery))); + + return Ok{}; + }))); + + return Ok{}; +} + +nsresult UpgradeCacheFrom1To2(mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + "ALTER TABLE origin ADD COLUMN suffix TEXT"_ns))); + + QM_TRY(InvalidateCache(aConnection)); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection)); + + MOZ_ASSERT(cacheVersion == 1); + } +#endif + + QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, 2))); + + return NS_OK; +} + +Result<bool, nsresult> MaybeCreateOrUpgradeCache( + mozIStorageConnection& aConnection) { + bool cacheUsable = true; + + QM_TRY_UNWRAP(int32_t cacheVersion, LoadCacheVersion(aConnection)); + + if (cacheVersion > kCacheVersion) { + cacheUsable = false; + } else if (cacheVersion != kCacheVersion) { + const bool newCache = !cacheVersion; + + mozStorageTransaction transaction( + &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + if (newCache) { + QM_TRY(MOZ_TO_RESULT(CreateCacheTables(aConnection))); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& cacheVersion, + LoadCacheVersion(aConnection)); + MOZ_ASSERT(cacheVersion == kCacheVersion); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO cache (valid, build_id) " + "VALUES (0, '')")))); + + nsCOMPtr<mozIStorageStatement> insertStmt; + + for (const PersistenceType persistenceType : kAllPersistenceTypes) { + if (insertStmt) { + MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset()); + } else { + QM_TRY_UNWRAP(insertStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, + aConnection, CreateStatement, + "INSERT INTO repository (id, name) " + "VALUES (:id, :name)"_ns)); + } + + QM_TRY(MOZ_TO_RESULT( + insertStmt->BindInt32ByName("id"_ns, persistenceType))); + + QM_TRY(MOZ_TO_RESULT(insertStmt->BindUTF8StringByName( + "name"_ns, PersistenceTypeToString(persistenceType)))); + + QM_TRY(MOZ_TO_RESULT(insertStmt->Execute())); + } + } else { + // This logic needs to change next time we change the cache! + static_assert(kCacheVersion == 2, + "Upgrade function needed due to cache version increase."); + + while (cacheVersion != kCacheVersion) { + if (cacheVersion == 1) { + QM_TRY(MOZ_TO_RESULT(UpgradeCacheFrom1To2(aConnection))); + } else { + QM_FAIL(Err(NS_ERROR_FAILURE), []() { + QM_WARNING( + "Unable to initialize cache, no upgrade path is " + "available!"); + }); + } + + QM_TRY_UNWRAP(cacheVersion, LoadCacheVersion(aConnection)); + } + + MOZ_ASSERT(cacheVersion == kCacheVersion); + } + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + } + + return cacheUsable; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateWebAppsStoreConnection( + nsIFile& aWebAppsStoreFile, mozIStorageService& aStorageService) { + AssertIsOnIOThread(); + + // Check if the old database exists at all. + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, Exists)); + + if (!exists) { + // webappsstore.sqlite doesn't exist, return a null connection. + return nsCOMPtr<mozIStorageConnection>{}; + } + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, IsDirectory)); + + if (isDirectory) { + QM_WARNING("webappsstore.sqlite is not a file!"); + return nsCOMPtr<mozIStorageConnection>{}; + } + + QM_TRY_INSPECT(const auto& connection, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, aStorageService, + OpenUnsharedDatabase, &aWebAppsStoreFile, + mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. Don't throw an error, leave a corrupted + // webappsstore database as it is. + ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>)); + + if (connection) { + // Don't propagate an error, leave a non-updateable webappsstore database as + // it is. + QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::Update(connection)), + nsCOMPtr<mozIStorageConnection>{}); + } + + return connection; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetLocalStorageArchiveFile( + const nsAString& aDirectoryPath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto lsArchiveFile, + QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aDirectoryPath))); + + QM_TRY(QM_TO_RESULT( + lsArchiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME)))); + + return lsArchiveFile; +} + +Result<nsCOMPtr<nsIFile>, nsresult> GetLocalStorageArchiveTmpFile( + const nsAString& aDirectoryPath) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aDirectoryPath.IsEmpty()); + + QM_TRY_UNWRAP(auto lsArchiveTmpFile, QM_NewLocalFile(aDirectoryPath)); + + QM_TRY(MOZ_TO_RESULT( + lsArchiveTmpFile->Append(nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME)))); + + return lsArchiveTmpFile; +} + +Result<bool, nsresult> IsLocalStorageArchiveInitialized( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, TableExists, "database"_ns)); +} + +nsresult InitializeLocalStorageArchive(mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const auto& initialized, + IsLocalStorageArchiveInitialized(*aConnection)); + MOZ_ASSERT(!initialized); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + "CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns))); + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement, + "INSERT INTO database (version) VALUES (:version)"_ns)); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, 0))); + QM_TRY(MOZ_TO_RESULT(stmt->Execute())); + + return NS_OK; +} + +Result<int32_t, nsresult> LoadLocalStorageArchiveVersion( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + aConnection, "SELECT version FROM database"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); +} + +nsresult SaveLocalStorageArchiveVersion(mozIStorageConnection* aConnection, + int32_t aVersion) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = aConnection->CreateStatement( + "UPDATE database SET version = :version;"_ns, getter_AddRefs(stmt)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->BindInt32ByName("version"_ns, aVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = stmt->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +template <typename FileFunc, typename DirectoryFunc> +Result<mozilla::Ok, nsresult> CollectEachFileEntry( + nsIFile& aDirectory, const FileFunc& aFileFunc, + const DirectoryFunc& aDirectoryFunc) { + AssertIsOnIOThread(); + + return CollectEachFile( + aDirectory, + [&aFileFunc, &aDirectoryFunc]( + const nsCOMPtr<nsIFile>& file) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + return aDirectoryFunc(file); + + case nsIFileKind::ExistsAsFile: + return aFileFunc(file); + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + }); +} + +/****************************************************************************** + * Quota manager class declarations + ******************************************************************************/ + +} // namespace + +class QuotaManager::Observer final : public nsIObserver { + static Observer* sInstance; + + bool mPendingProfileChange; + bool mShutdownComplete; + + public: + static nsresult Initialize(); + + static nsIObserver* GetInstance(); + + static void ShutdownCompleted(); + + private: + Observer() : mPendingProfileChange(false), mShutdownComplete(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + ~Observer() { MOZ_ASSERT(NS_IsMainThread()); } + + nsresult Init(); + + nsresult Shutdown(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER +}; + +namespace { + +/******************************************************************************* + * Local class declarations + ******************************************************************************/ + +} // namespace + +namespace { + +class CollectOriginsHelper final : public Runnable { + uint64_t mMinSizeToBeFreed; + + Mutex& mMutex; + CondVar mCondVar; + + // The members below are protected by mMutex. + nsTArray<RefPtr<OriginDirectoryLock>> mLocks; + uint64_t mSizeToBeFreed; + bool mWaiting; + + public: + CollectOriginsHelper(mozilla::Mutex& aMutex, uint64_t aMinSizeToBeFreed); + + // Blocks the current thread until origins are collected on the main thread. + // The returned value contains an aggregate size of those origins. + int64_t BlockAndReturnOriginsForEviction( + nsTArray<RefPtr<OriginDirectoryLock>>& aLocks); + + private: + ~CollectOriginsHelper() = default; + + NS_IMETHOD + Run() override; +}; + +class OriginOperationBase : public BackgroundThreadObject, public Runnable { + protected: + nsresult mResultCode; + + enum State { + // Not yet run. + State_Initial, + + // Running on the owning thread in the listener for OpenDirectory. + State_DirectoryOpenPending, + + // Running on the IO thread. + State_DirectoryWorkOpen, + + // Running on the owning thread after all work is done. + State_UnblockingOpen, + + // All done. + State_Complete + }; + + private: + State mState; + bool mActorDestroyed; + + protected: + bool mNeedsStorageInit; + + public: + void NoteActorDestroyed() { + AssertIsOnOwningThread(); + + mActorDestroyed = true; + } + + bool IsActorDestroyed() const { + AssertIsOnOwningThread(); + + return mActorDestroyed; + } + + protected: + explicit OriginOperationBase(nsISerialEventTarget* aOwningThread, + const char* aRunnableName) + : BackgroundThreadObject(aOwningThread), + Runnable(aRunnableName), + mResultCode(NS_OK), + mState(State_Initial), + mActorDestroyed(false), + mNeedsStorageInit(false) {} + + // Reference counted. + virtual ~OriginOperationBase() { + MOZ_ASSERT(mState == State_Complete); + MOZ_ASSERT(mActorDestroyed); + } + +#ifdef DEBUG + State GetState() const { return mState; } +#endif + + void SetState(State aState) { + MOZ_ASSERT(mState == State_Initial); + mState = aState; + } + + void AdvanceState() { + switch (mState) { + case State_Initial: + mState = State_DirectoryOpenPending; + return; + case State_DirectoryOpenPending: + mState = State_DirectoryWorkOpen; + return; + case State_DirectoryWorkOpen: + mState = State_UnblockingOpen; + return; + case State_UnblockingOpen: + mState = State_Complete; + return; + default: + MOZ_CRASH("Bad state!"); + } + } + + NS_IMETHOD + Run() override; + + virtual nsresult DoInit(QuotaManager& aQuotaManager); + + virtual void Open() = 0; + +#ifdef DEBUG + virtual nsresult DirectoryOpen(); +#else + nsresult DirectoryOpen(); +#endif + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) = 0; + + void Finish(nsresult aResult); + + virtual void UnblockOpen() = 0; + + private: + nsresult Init(); + + nsresult FinishInit(); + + nsresult DirectoryWork(); +}; + +class FinalizeOriginEvictionOp : public OriginOperationBase { + nsTArray<RefPtr<OriginDirectoryLock>> mLocks; + + public: + FinalizeOriginEvictionOp(nsISerialEventTarget* aBackgroundThread, + nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks) + : OriginOperationBase(aBackgroundThread, + "dom::quota::FinalizeOriginEvictionOp"), + mLocks(std::move(aLocks)) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + void Dispatch(); + + void RunOnIOThreadImmediately(); + + private: + ~FinalizeOriginEvictionOp() = default; + + virtual void Open() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void UnblockOpen() override; +}; + +class NormalOriginOperationBase + : public OriginOperationBase, + public OpenDirectoryListener, + public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + protected: + OriginScope mOriginScope; + RefPtr<DirectoryLock> mDirectoryLock; + Nullable<PersistenceType> mPersistenceType; + Nullable<Client::Type> mClientType; + mozilla::Atomic<bool> mCanceled; + const bool mExclusive; + + public: + void RunImmediately() { + MOZ_ASSERT(GetState() == State_Initial); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); + } + + protected: + NormalOriginOperationBase(const char* aRunnableName, + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type> aClientType, + bool aExclusive) + : OriginOperationBase(GetCurrentSerialEventTarget(), aRunnableName), + mOriginScope(aOriginScope), + mPersistenceType(aPersistenceType), + mClientType(aClientType), + mExclusive(aExclusive) { + AssertIsOnOwningThread(); + } + + ~NormalOriginOperationBase() = default; + + virtual RefPtr<DirectoryLock> CreateDirectoryLock(); + + private: + // Need to declare refcounting unconditionally, because + // OpenDirectoryListener has pure-virtual refcounting. + NS_DECL_ISUPPORTS_INHERITED + + virtual void Open() override; + + virtual void UnblockOpen() override; + + // OpenDirectoryListener overrides. + virtual void DirectoryLockAcquired(DirectoryLock* aLock) override; + + virtual void DirectoryLockFailed() override; + + // Used to send results before unblocking open. + virtual void SendResults() = 0; +}; + +class SaveOriginAccessTimeOp : public NormalOriginOperationBase { + const OriginMetadata mOriginMetadata; + int64_t mTimestamp; + + public: + SaveOriginAccessTimeOp(const OriginMetadata& aOriginMetadata, + int64_t aTimestamp) + : NormalOriginOperationBase( + "dom::quota::SaveOriginAccessTimeOp", + Nullable<PersistenceType>(aOriginMetadata.mPersistenceType), + OriginScope::FromOrigin(aOriginMetadata.mOrigin), + Nullable<Client::Type>(), + /* aExclusive */ false), + mOriginMetadata(aOriginMetadata), + mTimestamp(aTimestamp) { + AssertIsOnOwningThread(); + } + + private: + ~SaveOriginAccessTimeOp() = default; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void SendResults() override; +}; + +// XXX This class is a copy of ClearPrivateBrowsingOp because +// ClearPrivateBrowsingOp is supposed to work as a parent actor. We could maybe +// still inherit from ClearPrivateBrowsingOp instead of inheriting +// NormalOriginOperationBase and override SendResults, but that's still not +// very clean. It would be better to refactor the classes to have operations +// which can be used independently from IPC and then have wrappers (actors) +// around them for IPC. +class ClearPrivateRepositoryOp : public NormalOriginOperationBase { + MozPromiseHolder<BoolPromise> mPromiseHolder; + + public: + ClearPrivateRepositoryOp() + : NormalOriginOperationBase( + "dom::quota::ClearPrivateRepositoryOp", + Nullable<PersistenceType>(PERSISTENCE_TYPE_PRIVATE), + OriginScope::FromNull(), Nullable<Client::Type>(), + /* aExclusive */ true) { + AssertIsOnOwningThread(); + } + + RefPtr<BoolPromise> OnResults() { + AssertIsOnOwningThread(); + + return mPromiseHolder.Ensure(__func__); + } + + private: + ~ClearPrivateRepositoryOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void SendResults() override; +}; + +class ShutdownStorageOp : public NormalOriginOperationBase { + MozPromiseHolder<BoolPromise> mPromiseHolder; + + public: + ShutdownStorageOp() + : NormalOriginOperationBase( + "dom::quota::ShutdownStorageOp", Nullable<PersistenceType>(), + OriginScope::FromNull(), Nullable<Client::Type>(), + /* aExclusive */ true) { + AssertIsOnOwningThread(); + } + + RefPtr<BoolPromise> OnResults() { + AssertIsOnOwningThread(); + + return mPromiseHolder.Ensure(__func__); + } + + private: + ~ShutdownStorageOp() = default; + +#ifdef DEBUG + nsresult DirectoryOpen() override; +#endif + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void SendResults() override; +}; + +/******************************************************************************* + * Actor class declarations + ******************************************************************************/ + +class Quota final : public PQuotaParent { +#ifdef DEBUG + bool mActorDestroyed; +#endif + + public: + Quota(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::quota::Quota) + + private: + ~Quota(); + + bool VerifyRequestParams(const UsageRequestParams& aParams) const; + + bool VerifyRequestParams(const RequestParams& aParams) const; + + // IPDL methods. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual PQuotaUsageRequestParent* AllocPQuotaUsageRequestParent( + const UsageRequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult RecvPQuotaUsageRequestConstructor( + PQuotaUsageRequestParent* aActor, + const UsageRequestParams& aParams) override; + + virtual bool DeallocPQuotaUsageRequestParent( + PQuotaUsageRequestParent* aActor) override; + + virtual PQuotaRequestParent* AllocPQuotaRequestParent( + const RequestParams& aParams) override; + + virtual mozilla::ipc::IPCResult RecvPQuotaRequestConstructor( + PQuotaRequestParent* aActor, const RequestParams& aParams) override; + + virtual bool DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) override; + + virtual mozilla::ipc::IPCResult RecvStartIdleMaintenance() override; + + virtual mozilla::ipc::IPCResult RecvStopIdleMaintenance() override; + + virtual mozilla::ipc::IPCResult RecvAbortOperationsForProcess( + const ContentParentId& aContentParentId) override; +}; + +class QuotaUsageRequestBase : public NormalOriginOperationBase, + public PQuotaUsageRequestParent { + public: + // May be overridden by subclasses if they need to perform work on the + // background thread before being run. + virtual void Init(Quota& aQuota); + + protected: + QuotaUsageRequestBase(const char* aRunnableName) + : NormalOriginOperationBase(aRunnableName, Nullable<PersistenceType>(), + OriginScope::FromNull(), + Nullable<Client::Type>(), + /* aExclusive */ false) {} + + mozilla::Result<UsageInfo, nsresult> GetUsageForOrigin( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata); + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(UsageRequestResponse& aResponse) = 0; + + private: + mozilla::Result<UsageInfo, nsresult> GetUsageForOriginEntries( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, nsIFile& aDirectory, + bool aInitialized); + + void SendResults() override; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvCancel() final; +}; + +// A mix-in class to simplify operations that need to process every origin in +// one or more repositories. Sub-classes should call TraverseRepository in their +// DoDirectoryWork and implement a ProcessOrigin method for their per-origin +// logic. +class TraverseRepositoryHelper { + public: + TraverseRepositoryHelper() = default; + + protected: + virtual ~TraverseRepositoryHelper() = default; + + // If ProcessOrigin returns an error, TraverseRepository will immediately + // terminate and return the received error code to its caller. + nsresult TraverseRepository(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType); + + private: + virtual const Atomic<bool>& GetIsCanceledFlag() = 0; + + virtual nsresult ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, const bool aPersistent, + const PersistenceType aPersistenceType) = 0; +}; + +class GetUsageOp final : public QuotaUsageRequestBase, + public TraverseRepositoryHelper { + nsTArray<OriginUsage> mOriginUsages; + nsTHashMap<nsCStringHashKey, uint32_t> mOriginUsagesIndex; + + bool mGetAll; + + public: + explicit GetUsageOp(const UsageRequestParams& aParams); + + private: + ~GetUsageOp() = default; + + void ProcessOriginInternal(QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const nsACString& aOrigin, + const int64_t aTimestamp, const bool aPersisted, + const uint64_t aUsage); + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + const Atomic<bool>& GetIsCanceledFlag() override; + + nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) override; + + void GetResponse(UsageRequestResponse& aResponse) override; +}; + +class GetOriginUsageOp final : public QuotaUsageRequestBase { + const OriginUsageParams mParams; + nsCString mSuffix; + nsCString mGroup; + nsCString mStorageOrigin; + uint64_t mUsage; + uint64_t mFileUsage; + bool mIsPrivate; + bool mFromMemory; + + public: + explicit GetOriginUsageOp(const UsageRequestParams& aParams); + + private: + ~GetOriginUsageOp() = default; + + nsresult DoInit(QuotaManager& aQuotaManager) override; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(UsageRequestResponse& aResponse) override; +}; + +class QuotaRequestBase : public NormalOriginOperationBase, + public PQuotaRequestParent { + public: + // May be overridden by subclasses if they need to perform work on the + // background thread before being run. + virtual void Init(Quota& aQuota); + + protected: + explicit QuotaRequestBase(const char* aRunnableName, bool aExclusive) + : NormalOriginOperationBase(aRunnableName, Nullable<PersistenceType>(), + OriginScope::FromNull(), + Nullable<Client::Type>(), aExclusive) {} + + QuotaRequestBase(const char* aRunnableName, + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive) + : NormalOriginOperationBase(aRunnableName, aPersistenceType, aOriginScope, + aClientType, aExclusive) {} + + // Subclasses use this override to set the IPDL response value. + virtual void GetResponse(RequestResponse& aResponse) = 0; + + private: + virtual void SendResults() override; + + // IPDL methods. + virtual void ActorDestroy(ActorDestroyReason aWhy) override; +}; + +class StorageNameOp final : public QuotaRequestBase { + nsString mName; + + public: + StorageNameOp(); + + void Init(Quota& aQuota) override; + + private: + ~StorageNameOp() = default; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializedRequestBase : public QuotaRequestBase { + protected: + bool mInitialized; + + public: + void Init(Quota& aQuota) override; + + protected: + InitializedRequestBase(const char* aRunnableName); + + private: + RefPtr<DirectoryLock> CreateDirectoryLock() override; +}; + +class StorageInitializedOp final : public InitializedRequestBase { + public: + StorageInitializedOp() + : InitializedRequestBase("dom::quota::StorageInitializedOp") {} + + private: + ~StorageInitializedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class TemporaryStorageInitializedOp final : public InitializedRequestBase { + public: + TemporaryStorageInitializedOp() + : InitializedRequestBase("dom::quota::StorageInitializedOp") {} + + private: + ~TemporaryStorageInitializedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitOp final : public QuotaRequestBase { + public: + InitOp(); + + void Init(Quota& aQuota) override; + + private: + ~InitOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitTemporaryStorageOp final : public QuotaRequestBase { + public: + InitTemporaryStorageOp(); + + void Init(Quota& aQuota) override; + + private: + ~InitTemporaryStorageOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializeOriginRequestBase : public QuotaRequestBase { + protected: + const PrincipalInfo mPrincipalInfo; + nsCString mSuffix; + nsCString mGroup; + nsCString mStorageOrigin; + bool mIsPrivate; + bool mCreated; + + public: + void Init(Quota& aQuota) override; + + protected: + InitializeOriginRequestBase(const char* aRunnableName, + PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo); + + nsresult DoInit(QuotaManager& aQuotaManager) override; +}; + +class InitializePersistentOriginOp final : public InitializeOriginRequestBase { + public: + explicit InitializePersistentOriginOp(const RequestParams& aParams); + + private: + ~InitializePersistentOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class InitializeTemporaryOriginOp final : public InitializeOriginRequestBase { + public: + explicit InitializeTemporaryOriginOp(const RequestParams& aParams); + + private: + ~InitializeTemporaryOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class GetFullOriginMetadataOp : public QuotaRequestBase { + const GetFullOriginMetadataParams mParams; + // XXX Consider wrapping with LazyInitializedOnce + OriginMetadata mOriginMetadata; + Maybe<FullOriginMetadata> mMaybeFullOriginMetadata; + + public: + explicit GetFullOriginMetadataOp(const GetFullOriginMetadataParams& aParams); + + private: + nsresult DoInit(QuotaManager& aQuotaManager) override; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ResetOrClearOp final : public QuotaRequestBase { + const bool mClear; + + public: + explicit ResetOrClearOp(bool aClear); + + void Init(Quota& aQuota) override; + + private: + ~ResetOrClearOp() = default; + + void DeleteFiles(QuotaManager& aQuotaManager); + + void DeleteStorageFile(QuotaManager& aQuotaManager); + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + virtual void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearPrivateBrowsingOp final : public QuotaRequestBase { + public: + ClearPrivateBrowsingOp(); + + private: + ~ClearPrivateBrowsingOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearRequestBase : public QuotaRequestBase { + protected: + explicit ClearRequestBase(const char* aRunnableName, bool aExclusive) + : QuotaRequestBase(aRunnableName, aExclusive) { + AssertIsOnOwningThread(); + } + + ClearRequestBase(const char* aRunnableName, + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType, bool aExclusive) + : QuotaRequestBase(aRunnableName, aPersistenceType, aOriginScope, + aClientType, aExclusive) {} + + void DeleteFiles(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType); + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; +}; + +class ClearOriginOp final : public ClearRequestBase { + const ClearResetOriginParams mParams; + const bool mMatchAll; + + public: + explicit ClearOriginOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ClearOriginOp() = default; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ClearDataOp final : public ClearRequestBase { + const ClearDataParams mParams; + + public: + explicit ClearDataOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ClearDataOp() = default; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ResetOriginOp final : public QuotaRequestBase { + public: + explicit ResetOriginOp(const RequestParams& aParams); + + void Init(Quota& aQuota) override; + + private: + ~ResetOriginOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class PersistRequestBase : public QuotaRequestBase { + const PrincipalInfo mPrincipalInfo; + + protected: + nsCString mSuffix; + nsCString mGroup; + nsCString mStorageOrigin; + bool mIsPrivate; + + public: + void Init(Quota& aQuota) override; + + protected: + explicit PersistRequestBase(const PrincipalInfo& aPrincipalInfo); + + nsresult DoInit(QuotaManager& aQuotaManager) override; +}; + +class PersistedOp final : public PersistRequestBase { + bool mPersisted; + + public: + explicit PersistedOp(const RequestParams& aParams); + + private: + ~PersistedOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class PersistOp final : public PersistRequestBase { + public: + explicit PersistOp(const RequestParams& aParams); + + private: + ~PersistOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class EstimateOp final : public QuotaRequestBase { + const EstimateParams mParams; + OriginMetadata mOriginMetadata; + std::pair<uint64_t, uint64_t> mUsageAndLimit; + + public: + explicit EstimateOp(const EstimateParams& aParams); + + private: + ~EstimateOp() = default; + + nsresult DoInit(QuotaManager& aQuotaManager) override; + + RefPtr<DirectoryLock> CreateDirectoryLock() override; + + virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +class ListOriginsOp final : public QuotaRequestBase, + public TraverseRepositoryHelper { + // XXX Bug 1521541 will make each origin has it's own state. + nsTArray<nsCString> mOrigins; + + public: + ListOriginsOp(); + + void Init(Quota& aQuota) override; + + private: + ~ListOriginsOp() = default; + + nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override; + + const Atomic<bool>& GetIsCanceledFlag() override; + + nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) override; + + void GetResponse(RequestResponse& aResponse) override; +}; + +/******************************************************************************* + * Other class declarations + ******************************************************************************/ + +class StoragePressureRunnable final : public Runnable { + const uint64_t mUsage; + + public: + explicit StoragePressureRunnable(uint64_t aUsage) + : Runnable("dom::quota::QuotaObject::StoragePressureRunnable"), + mUsage(aUsage) {} + + private: + ~StoragePressureRunnable() = default; + + NS_DECL_NSIRUNNABLE +}; + +class RecordQuotaInfoLoadTimeHelper final : public Runnable { + // TimeStamps that are set on the IO thread. + LazyInitializedOnceNotNull<const TimeStamp> mStartTime; + LazyInitializedOnceNotNull<const TimeStamp> mEndTime; + + // A TimeStamp that is set on the main thread. + LazyInitializedOnceNotNull<const TimeStamp> mInitializedTime; + + public: + RecordQuotaInfoLoadTimeHelper() + : Runnable("dom::quota::RecordQuotaInfoLoadTimeHelper") {} + + TimeStamp Start(); + + TimeStamp End(); + + private: + ~RecordQuotaInfoLoadTimeHelper() = default; + + NS_DECL_NSIRUNNABLE +}; + +/******************************************************************************* + * Helper classes + ******************************************************************************/ + +/******************************************************************************* + * Helper Functions + ******************************************************************************/ + +inline bool IsDotFile(const nsAString& aFileName) { + return QuotaManager::IsDotFile(aFileName); +} + +inline bool IsOSMetadata(const nsAString& aFileName) { + return QuotaManager::IsOSMetadata(aFileName); +} + +bool IsOriginMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(METADATA_FILE_NAME) || + aFileName.EqualsLiteral(METADATA_V2_FILE_NAME) || + IsOSMetadata(aFileName); +} + +bool IsTempMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(METADATA_TMP_FILE_NAME) || + aFileName.EqualsLiteral(METADATA_V2_TMP_FILE_NAME); +} + +// Return whether the group was actually updated. +Result<bool, nsresult> MaybeUpdateGroupForOrigin( + OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(!NS_IsMainThread()); + + bool updated = false; + + if (aOriginMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) { + if (!aOriginMetadata.mGroup.EqualsLiteral(kChromeOrigin)) { + aOriginMetadata.mGroup.AssignLiteral(kChromeOrigin); + updated = true; + } + } else { + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aOriginMetadata.mOrigin); + QM_TRY(MOZ_TO_RESULT(principal)); + + QM_TRY_INSPECT(const auto& baseDomain, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal, + GetBaseDomain)); + + const nsCString upToDateGroup = baseDomain + aOriginMetadata.mSuffix; + + if (aOriginMetadata.mGroup != upToDateGroup) { + aOriginMetadata.mGroup = upToDateGroup; + updated = true; + } + } + + return updated; +} + +Result<bool, nsresult> MaybeUpdateLastAccessTimeForOrigin( + FullOriginMetadata& aFullOriginMetadata) { + MOZ_ASSERT(!NS_IsMainThread()); + + if (aFullOriginMetadata.mLastAccessTime == INT64_MIN) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_INSPECT(const auto& metadataFile, + quotaManager->GetOriginDirectory(aFullOriginMetadata)); + + QM_TRY(MOZ_TO_RESULT( + metadataFile->Append(nsLiteralString(METADATA_V2_FILE_NAME)))); + + QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER( + metadataFile, GetLastModifiedTime)); + + // Need to convert from milliseconds to microseconds. + MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp); + timestamp *= int64_t(PR_USEC_PER_MSEC); + + aFullOriginMetadata.mLastAccessTime = timestamp; + + return true; + } + + return false; +} + +} // namespace + +BackgroundThreadObject::BackgroundThreadObject() + : mOwningThread(GetCurrentSerialEventTarget()) { + AssertIsOnOwningThread(); +} + +BackgroundThreadObject::BackgroundThreadObject( + nsISerialEventTarget* aOwningThread) + : mOwningThread(aOwningThread) {} + +#ifdef DEBUG + +void BackgroundThreadObject::AssertIsOnOwningThread() const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mOwningThread); + bool current; + MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(¤t))); + MOZ_ASSERT(current); +} + +#endif // DEBUG + +nsISerialEventTarget* BackgroundThreadObject::OwningThread() const { + MOZ_ASSERT(mOwningThread); + return mOwningThread; +} + +bool IsOnIOThread() { + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + bool currentThread; + return NS_SUCCEEDED( + quotaManager->IOThread()->IsOnCurrentThread(¤tThread)) && + currentThread; +} + +void AssertIsOnIOThread() { + NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!"); +} + +void DiagnosticAssertIsOnIOThread() { MOZ_DIAGNOSTIC_ASSERT(IsOnIOThread()); } + +void AssertCurrentThreadOwnsQuotaMutex() { +#ifdef DEBUG + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Must have a manager here!"); + + quotaManager->AssertCurrentThreadOwnsQuotaMutex(); +#endif +} + +void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) { + // Get leaf of file path + for (const char* p = aFile; *p; ++p) { + if (*p == '/' && *(p + 1)) { + aFile = p + 1; + } + } + + nsContentUtils::LogSimpleConsoleError( + NS_ConvertUTF8toUTF16( + nsPrintfCString("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)), + "quota"_ns, + false /* Quota Manager is not active in private browsing mode */, + true /* Quota Manager runs always in a chrome context */); +} + +namespace { + +bool gInvalidateQuotaCache = false; +StaticAutoPtr<nsString> gBasePath; +StaticAutoPtr<nsString> gStorageName; +StaticAutoPtr<nsCString> gBuildId; + +#ifdef DEBUG +bool gQuotaManagerInitialized = false; +#endif + +StaticRefPtr<QuotaManager> gInstance; +mozilla::Atomic<bool> gShutdown(false); + +// A time stamp that can only be accessed on the main thread. +TimeStamp gLastOSWake; + +using NormalOriginOpArray = + nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>; +StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps; + +void RegisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + + if (!gNormalOriginOps) { + gNormalOriginOps = new NormalOriginOpArray(); + } + + gNormalOriginOps->AppendElement(&aNormalOriginOp); +} + +void UnregisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gNormalOriginOps); + + gNormalOriginOps->RemoveElement(&aNormalOriginOp); + + if (gNormalOriginOps->IsEmpty()) { + gNormalOriginOps = nullptr; + } +} + +class StorageOperationBase { + protected: + struct OriginProps { + enum Type { eChrome, eContent, eObsolete, eInvalid }; + + NotNull<nsCOMPtr<nsIFile>> mDirectory; + nsString mLeafName; + nsCString mSpec; + OriginAttributes mAttrs; + int64_t mTimestamp; + OriginMetadata mOriginMetadata; + nsCString mOriginalSuffix; + + LazyInitializedOnceEarlyDestructible<const PersistenceType> + mPersistenceType; + Type mType; + bool mNeedsRestore; + bool mNeedsRestore2; + bool mIgnore; + + public: + explicit OriginProps(MovingNotNull<nsCOMPtr<nsIFile>> aDirectory) + : mDirectory(std::move(aDirectory)), + mTimestamp(0), + mType(eContent), + mNeedsRestore(false), + mNeedsRestore2(false), + mIgnore(false) {} + + template <typename PersistenceTypeFunc> + nsresult Init(PersistenceTypeFunc&& aPersistenceTypeFunc); + }; + + nsTArray<OriginProps> mOriginProps; + + nsCOMPtr<nsIFile> mDirectory; + + public: + explicit StorageOperationBase(nsIFile* aDirectory) : mDirectory(aDirectory) { + AssertIsOnIOThread(); + } + + NS_INLINE_DECL_REFCOUNTING(StorageOperationBase) + + protected: + virtual ~StorageOperationBase() = default; + + nsresult GetDirectoryMetadata(nsIFile* aDirectory, int64_t& aTimestamp, + nsACString& aGroup, nsACString& aOrigin, + Nullable<bool>& aIsApp); + + // Upgrade helper to load the contents of ".metadata-v2" files from previous + // schema versions. Although QuotaManager has a similar GetDirectoryMetadata2 + // method, it is only intended to read current version ".metadata-v2" files. + // And unlike the old ".metadata" files, the ".metadata-v2" format can evolve + // because our "storage.sqlite" lets us track the overall version of the + // storage directory. + nsresult GetDirectoryMetadata2(nsIFile* aDirectory, int64_t& aTimestamp, + nsACString& aSuffix, nsACString& aGroup, + nsACString& aOrigin, bool& aIsApp); + + int64_t GetOriginLastModifiedTime(const OriginProps& aOriginProps); + + nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps); + + /** + * Rename the origin if the origin string generation from nsIPrincipal + * changed. This consists of renaming the origin in the metadata files and + * renaming the origin directory itself. For simplicity, the origin in + * metadata files is not actually updated, but the metadata files are + * recreated instead. + * + * @param aOriginProps the properties of the origin to check. + * + * @return whether origin was renamed. + */ + Result<bool, nsresult> MaybeRenameOrigin(const OriginProps& aOriginProps); + + nsresult ProcessOriginDirectories(); + + virtual nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) = 0; +}; + +class MOZ_STACK_CLASS OriginParser final { + public: + enum ResultType { InvalidOrigin, ObsoleteOrigin, ValidOrigin }; + + private: + using Tokenizer = + nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>; + + enum SchemeType { eNone, eFile, eAbout, eChrome }; + + enum State { + eExpectingAppIdOrScheme, + eExpectingInMozBrowser, + eExpectingScheme, + eExpectingEmptyToken1, + eExpectingEmptyToken2, + eExpectingEmptyTokenOrUniversalFileOrigin, + eExpectingHost, + eExpectingPort, + eExpectingEmptyTokenOrDriveLetterOrPathnameComponent, + eExpectingEmptyTokenOrPathnameComponent, + eExpectingEmptyToken1OrHost, + + // We transit from eExpectingHost to this state when we encounter a host + // beginning with "[" which indicates an IPv6 literal. Because we mangle the + // IPv6 ":" delimiter to be a "+", we will receive separate tokens for each + // portion of the IPv6 address, including a final token that ends with "]". + // (Note that we do not mangle "[" or "]".) Note that the URL spec + // explicitly disclaims support for "<zone_id>" and so we don't have to deal + // with that. + eExpectingIPV6Token, + eComplete, + eHandledTrailingSeparator + }; + + const nsCString mOrigin; + Tokenizer mTokenizer; + + nsCString mScheme; + nsCString mHost; + Nullable<uint32_t> mPort; + nsTArray<nsCString> mPathnameComponents; + nsCString mHandledTokens; + + SchemeType mSchemeType; + State mState; + bool mInIsolatedMozBrowser; + bool mUniversalFileOrigin; + bool mMaybeDriveLetter; + bool mError; + bool mMaybeObsolete; + + // Number of group which a IPv6 address has. Should be less than 9. + uint8_t mIPGroup; + + public: + explicit OriginParser(const nsACString& aOrigin) + : mOrigin(aOrigin), + mTokenizer(aOrigin, '+'), + mPort(), + mSchemeType(eNone), + mState(eExpectingAppIdOrScheme), + mInIsolatedMozBrowser(false), + mUniversalFileOrigin(false), + mMaybeDriveLetter(false), + mError(false), + mMaybeObsolete(false), + mIPGroup(0) {} + + static ResultType ParseOrigin(const nsACString& aOrigin, nsCString& aSpec, + OriginAttributes* aAttrs, + nsCString& aOriginalSuffix); + + ResultType Parse(nsACString& aSpec); + + private: + void HandleScheme(const nsDependentCSubstring& aToken); + + void HandlePathnameComponent(const nsDependentCSubstring& aToken); + + void HandleToken(const nsDependentCSubstring& aToken); + + void HandleTrailingSeparator(); +}; + +class RepositoryOperationBase : public StorageOperationBase { + public: + explicit RepositoryOperationBase(nsIFile* aDirectory) + : StorageOperationBase(aDirectory) {} + + nsresult ProcessRepository(); + + protected: + virtual ~RepositoryOperationBase() = default; + + template <typename UpgradeMethod> + nsresult MaybeUpgradeClients(const OriginProps& aOriginsProps, + UpgradeMethod aMethod); + + private: + virtual PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) = 0; + + virtual nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) = 0; + + virtual nsresult PrepareClientDirectory(nsIFile* aFile, + const nsAString& aLeafName, + bool& aRemoved); +}; + +class CreateOrUpgradeDirectoryMetadataHelper final + : public RepositoryOperationBase { + nsCOMPtr<nsIFile> mPermanentStorageDir; + + // The legacy PersistenceType, before the default repository introduction. + enum class LegacyPersistenceType { + Persistent = 0, + Temporary + // The PersistenceType had also PERSISTENCE_TYPE_INVALID, but we don't need + // it here. + }; + + LazyInitializedOnce<const LegacyPersistenceType> mLegacyPersistenceType; + + public: + explicit CreateOrUpgradeDirectoryMetadataHelper(nsIFile* aDirectory) + : RepositoryOperationBase(aDirectory) {} + + nsresult Init(); + + private: + Maybe<LegacyPersistenceType> LegacyPersistenceTypeFromFile(nsIFile& aFile, + const fallible_t&); + + PersistenceType PersistenceTypeFromLegacyPersistentSpec( + const nsCString& aSpec); + + PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override; + + nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory); + + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageHelperBase : public RepositoryOperationBase { + LazyInitializedOnce<const PersistenceType> mPersistenceType; + + public: + explicit UpgradeStorageHelperBase(nsIFile* aDirectory) + : RepositoryOperationBase(aDirectory) {} + + nsresult Init(); + + private: + PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override; +}; + +class UpgradeStorageFrom0_0To1_0Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom0_0To1_0Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom1_0To2_0Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom1_0To2_0Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult MaybeRemoveMorgueDirectory(const OriginProps& aOriginProps); + + /** + * Remove the origin directory if appId is present in origin attributes. + * + * @param aOriginProps the properties of the origin to check. + * + * @return whether the origin directory was removed. + */ + Result<bool, nsresult> MaybeRemoveAppsData(const OriginProps& aOriginProps); + + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom2_0To2_1Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +class UpgradeStorageFrom2_1To2_2Helper final : public UpgradeStorageHelperBase { + public: + explicit UpgradeStorageFrom2_1To2_2Helper(nsIFile* aDirectory) + : UpgradeStorageHelperBase(aDirectory) {} + + private: + nsresult PrepareOriginDirectory(OriginProps& aOriginProps, + bool* aRemoved) override; + + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; + + nsresult PrepareClientDirectory(nsIFile* aFile, const nsAString& aLeafName, + bool& aRemoved) override; +}; + +class RestoreDirectoryMetadata2Helper final : public StorageOperationBase { + LazyInitializedOnce<const PersistenceType> mPersistenceType; + + public: + explicit RestoreDirectoryMetadata2Helper(nsIFile* aDirectory) + : StorageOperationBase(aDirectory) {} + + nsresult Init(); + + nsresult RestoreMetadata2File(); + + private: + nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override; +}; + +auto MakeSanitizedOriginCString(const nsACString& aOrigin) { +#ifdef XP_WIN + NS_ASSERTION(!strcmp(QuotaManager::kReplaceChars, + FILE_ILLEGAL_CHARACTERS FILE_PATH_SEPARATOR), + "Illegal file characters have changed!"); +#endif + + nsAutoCString res{aOrigin}; + + res.ReplaceChar(QuotaManager::kReplaceChars, '+'); + + return res; +} + +auto MakeSanitizedOriginString(const nsACString& aOrigin) { + // An origin string is ASCII-only, since it is obtained via + // nsIPrincipal::GetOrigin, which returns an ACString. + return NS_ConvertASCIItoUTF16(MakeSanitizedOriginCString(aOrigin)); +} + +Result<nsAutoString, nsresult> GetPathForStorage( + nsIFile& aBaseDir, const nsAString& aStorageName) { + QM_TRY_INSPECT(const auto& storageDir, + CloneFileAndAppend(aBaseDir, aStorageName)); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, storageDir, GetPath)); +} + +int64_t GetLastModifiedTime(PersistenceType aPersistenceType, nsIFile& aFile) { + AssertIsOnIOThread(); + + class MOZ_STACK_CLASS Helper final { + public: + static nsresult GetLastModifiedTime(nsIFile* aFile, int64_t* aTimestamp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aFile); + MOZ_ASSERT(aTimestamp); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*aFile)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + QM_TRY(CollectEachFile( + *aFile, + [&aTimestamp](const nsCOMPtr<nsIFile>& file) + -> Result<mozilla::Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(GetLastModifiedTime(file, aTimestamp))); + + return Ok{}; + })); + break; + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aFile, + GetLeafName)); + + // Bug 1595445 will handle unknown files here. + + if (IsOriginMetadata(leafName) || IsTempMetadata(leafName) || + IsDotFile(leafName)) { + return NS_OK; + } + + QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER( + aFile, GetLastModifiedTime)); + + // Need to convert from milliseconds to microseconds. + MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp); + timestamp *= int64_t(PR_USEC_PER_MSEC); + + if (timestamp > *aTimestamp) { + *aTimestamp = timestamp; + } + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return NS_OK; + } + }; + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + return PR_Now(); + } + + int64_t timestamp = INT64_MIN; + nsresult rv = Helper::GetLastModifiedTime(&aFile, ×tamp); + if (NS_FAILED(rv)) { + timestamp = PR_Now(); + } + + // XXX if there were no suitable files for getting last modified time + // (timestamp is still set to INT64_MIN), we should return the current time + // instead of returning INT64_MIN. + + return timestamp; +} + +// Returns a bool indicating whether the directory was newly created. +Result<bool, nsresult> EnsureDirectory(nsIFile& aDirectory) { + AssertIsOnIOThread(); + + // Callers call this function without checking if the directory already + // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we + // just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the + // reports. + QM_TRY_INSPECT(const auto& exists, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Create, + nsIFile::DIRECTORY_TYPE, 0755, + /* aSkipAncestors = */ false) + .map([](Ok) { return false; }), + // Predicate. + IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>, + // Fallback. + ErrToOk<true>)); + + if (exists) { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory)); + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED)); + } + + return !exists; +} + +enum FileFlag { Truncate, Update, Append }; + +Result<nsCOMPtr<nsIOutputStream>, nsresult> GetOutputStream( + nsIFile& aFile, FileFlag aFileFlag) { + AssertIsOnIOThread(); + + switch (aFileFlag) { + case FileFlag::Truncate: + QM_TRY_RETURN(NS_NewLocalFileOutputStream(&aFile)); + + case FileFlag::Update: { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(&aFile, Exists)); + + if (!exists) { + return nsCOMPtr<nsIOutputStream>(); + } + + QM_TRY_INSPECT(const auto& stream, + NS_NewLocalFileRandomAccessStream(&aFile)); + + nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(stream); + QM_TRY(OkIf(outputStream), Err(NS_ERROR_FAILURE)); + + return outputStream; + } + + case FileFlag::Append: + QM_TRY_RETURN(NS_NewLocalFileOutputStream( + &aFile, PR_WRONLY | PR_CREATE_FILE | PR_APPEND)); + + default: + MOZ_CRASH("Should never get here!"); + } +} + +Result<nsCOMPtr<nsIBinaryOutputStream>, nsresult> GetBinaryOutputStream( + nsIFile& aFile, FileFlag aFileFlag) { + QM_TRY_UNWRAP(auto outputStream, GetOutputStream(aFile, aFileFlag)); + + QM_TRY(OkIf(outputStream), Err(NS_ERROR_UNEXPECTED)); + + return nsCOMPtr<nsIBinaryOutputStream>( + NS_NewObjectOutputStream(outputStream)); +} + +void GetJarPrefix(bool aInIsolatedMozBrowser, nsACString& aJarPrefix) { + aJarPrefix.Truncate(); + + // Fallback. + if (!aInIsolatedMozBrowser) { + return; + } + + // AppId is an unused b2g identifier. Let's set it to 0 all the time (see bug + // 1320404). + // aJarPrefix = appId + "+" + { 't', 'f' } + "+"; + aJarPrefix.AppendInt(0); // TODO: this is the appId, to be removed. + aJarPrefix.Append('+'); + aJarPrefix.Append(aInIsolatedMozBrowser ? 't' : 'f'); + aJarPrefix.Append('+'); +} + +nsresult CreateDirectoryMetadata(nsIFile& aDirectory, int64_t aTimestamp, + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + OriginAttributes groupAttributes; + + nsCString groupNoSuffix; + QM_TRY(OkIf(groupAttributes.PopulateFromOrigin(aOriginMetadata.mGroup, + groupNoSuffix)), + NS_ERROR_FAILURE); + + nsCString groupPrefix; + GetJarPrefix(groupAttributes.mInIsolatedMozBrowser, groupPrefix); + + nsCString group = groupPrefix + groupNoSuffix; + + OriginAttributes originAttributes; + + nsCString originNoSuffix; + QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin, + originNoSuffix)), + NS_ERROR_FAILURE); + + nsCString originPrefix; + GetJarPrefix(originAttributes.mInIsolatedMozBrowser, originPrefix); + + nsCString origin = originPrefix + originNoSuffix; + + MOZ_ASSERT(groupPrefix == originPrefix); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_TMP_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Truncate)); + MOZ_ASSERT(stream); + + QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(group.get()))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(origin.get()))); + + // Currently unused (used to be isApp). + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false))); + + QM_TRY(MOZ_TO_RESULT(stream->Flush())); + + QM_TRY(MOZ_TO_RESULT(stream->Close())); + + QM_TRY(MOZ_TO_RESULT( + file->RenameTo(nullptr, nsLiteralString(METADATA_FILE_NAME)))); + + return NS_OK; +} + +nsresult CreateDirectoryMetadata2(nsIFile& aDirectory, int64_t aTimestamp, + bool aPersisted, + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY( + MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_TMP_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Truncate)); + MOZ_ASSERT(stream); + + QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp))); + + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aPersisted))); + + // Reserved data 1 + QM_TRY(MOZ_TO_RESULT(stream->Write32(0))); + + // Reserved data 2 + QM_TRY(MOZ_TO_RESULT(stream->Write32(0))); + + // Currently unused (used to be suffix). + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(""))); + + // Currently unused (used to be group). + QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(""))); + + QM_TRY(MOZ_TO_RESULT( + stream->WriteStringZ(aOriginMetadata.mStorageOrigin.get()))); + + // Currently used for isPrivate (used to be used for isApp). + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aOriginMetadata.mIsPrivate))); + + QM_TRY(MOZ_TO_RESULT(stream->Flush())); + + QM_TRY(MOZ_TO_RESULT(stream->Close())); + + QM_TRY(MOZ_TO_RESULT( + file->RenameTo(nullptr, nsLiteralString(METADATA_V2_FILE_NAME)))); + + return NS_OK; +} + +Result<nsCOMPtr<nsIBinaryInputStream>, nsresult> GetBinaryInputStream( + nsIFile& aDirectory, const nsAString& aFilename) { + MOZ_ASSERT(!NS_IsMainThread()); + + QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, aDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(file->Append(aFilename))); + + QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file)); + + QM_TRY_INSPECT(const auto& bufferedStream, + NS_NewBufferedInputStream(stream.forget(), 512)); + + QM_TRY(OkIf(bufferedStream), Err(NS_ERROR_FAILURE)); + + return nsCOMPtr<nsIBinaryInputStream>( + NS_NewObjectInputStream(bufferedStream)); +} + +// This method computes and returns our best guess for the temporary storage +// limit (in bytes), based on disk capacity. +Result<uint64_t, nsresult> GetTemporaryStorageLimit(nsIFile& aStorageDir) { + // The fixed limit pref can be used to override temporary storage limit + // calculation. + if (StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit() >= 0) { + return static_cast<uint64_t>( + StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit()) * + 1024; + } + + constexpr int64_t teraByte = (1024LL * 1024LL * 1024LL * 1024LL); + constexpr int64_t maxAllowedCapacity = 8LL * teraByte; + + // Check for disk capacity of user's device on which storage directory lives. + int64_t diskCapacity = maxAllowedCapacity; + + // Log error when default disk capacity is returned due to the error + QM_WARNONLY_TRY(MOZ_TO_RESULT(aStorageDir.GetDiskCapacity(&diskCapacity))); + + MOZ_ASSERT(diskCapacity >= 0LL); + + // Allow temporary storage to consume up to 50% of disk capacity. + int64_t capacityLimit = diskCapacity / 2LL; + + // If the disk capacity reported by the operating system is very + // large and potentially incorrect due to hardware issues, + // a hardcoded limit is supplied instead. + QM_WARNONLY_TRY( + OkIf(capacityLimit < maxAllowedCapacity), + ([&capacityLimit](const auto&) { capacityLimit = maxAllowedCapacity; })); + + return capacityLimit; +} + +bool IsOriginUnaccessed(const FullOriginMetadata& aFullOriginMetadata, + const int64_t aRecentTime) { + if (aFullOriginMetadata.mLastAccessTime > aRecentTime) { + return false; + } + + return (aRecentTime - aFullOriginMetadata.mLastAccessTime) / PR_USEC_PER_SEC > + StaticPrefs::dom_quotaManager_unaccessedForLongTimeThresholdSec(); +} + +} // namespace + +/******************************************************************************* + * Exported functions + ******************************************************************************/ + +void InitializeQuotaManager() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gQuotaManagerInitialized); + +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + ScopedLogExtraInfo::Initialize(); +#endif + + if (!QuotaManager::IsRunningGTests()) { + // These services have to be started on the main thread currently. + const nsCOMPtr<mozIStorageService> ss = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + QM_WARNONLY_TRY(OkIf(ss)); + + RefPtr<net::ExtensionProtocolHandler> extensionProtocolHandler = + net::ExtensionProtocolHandler::GetSingleton(); + QM_WARNONLY_TRY(MOZ_TO_RESULT(extensionProtocolHandler)); + } + + QM_WARNONLY_TRY(QM_TO_RESULT(QuotaManager::Initialize())); + +#ifdef DEBUG + gQuotaManagerInitialized = true; +#endif +} + +PQuotaParent* AllocPQuotaParent() { + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + + auto actor = MakeRefPtr<Quota>(); + + return actor.forget().take(); +} + +bool DeallocPQuotaParent(PQuotaParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + RefPtr<Quota> actor = dont_AddRef(static_cast<Quota*>(aActor)); + return true; +} + +bool RecvShutdownQuotaManager() { + AssertIsOnBackgroundThread(); + + // If we are already in shutdown, don't call ShutdownInstance() + // again and return true immediately. We shall see this incident + // in Telemetry. + // XXX todo: Make QM_TRY stacks thread-aware (Bug 1735124) + // XXX todo: Active QM_TRY context for shutdown (Bug 1735170) + QM_TRY(OkIf(!gShutdown), true); + + QuotaManager::ShutdownInstance(); + + return true; +} + +QuotaManager::Observer* QuotaManager::Observer::sInstance = nullptr; + +// static +nsresult QuotaManager::Observer::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<Observer> observer = new Observer(); + + nsresult rv = observer->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + sInstance = observer; + + return NS_OK; +} + +// static +nsIObserver* QuotaManager::Observer::GetInstance() { + MOZ_ASSERT(NS_IsMainThread()); + + return sInstance; +} + +// static +void QuotaManager::Observer::ShutdownCompleted() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(sInstance); + + sInstance->mShutdownComplete = true; +} + +nsresult QuotaManager::Observer::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + // XXX: Improve the way that we remove observer in failure cases. + nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = obs->AddObserver(this, kProfileDoChangeTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + rv = obs->AddObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, kProfileDoChangeTopic); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + rv = obs->AddObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID); + obs->RemoveObserver(this, kProfileDoChangeTopic); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + rv = obs->AddObserver(this, kPrivateBrowsingObserverTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + obs->RemoveObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC); + obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID); + obs->RemoveObserver(this, kProfileDoChangeTopic); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + return rv; + } + + return NS_OK; +} + +nsresult QuotaManager::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_WIDGET_WAKE_OBSERVER_TOPIC)); + MOZ_ALWAYS_SUCCEEDS( + obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)); + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kProfileDoChangeTopic)); + MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID)); + + sInstance = nullptr; + + // 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(QuotaManager::Observer, nsIObserver) + +NS_IMETHODIMP +QuotaManager::Observer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + if (!strcmp(aTopic, kProfileDoChangeTopic)) { + if (NS_WARN_IF(gBasePath)) { + NS_WARNING( + "profile-before-change-qm must precede repeated " + "profile-do-change!"); + return NS_OK; + } + + Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, true); + + gBasePath = new nsString(); + + nsCOMPtr<nsIFile> baseDir; + rv = NS_GetSpecialDirectory(NS_APP_INDEXEDDB_PARENT_DIR, + getter_AddRefs(baseDir)); + if (NS_FAILED(rv)) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(baseDir)); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = baseDir->GetPath(*gBasePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + gStorageName = new nsString(); + + rv = Preferences::GetString("dom.quotaManager.storageName", *gStorageName); + if (NS_FAILED(rv)) { + *gStorageName = kStorageName; + } + + gBuildId = new nsCString(); + + nsCOMPtr<nsIPlatformInfo> platformInfo = + do_GetService("@mozilla.org/xre/app-info;1"); + if (NS_WARN_IF(!platformInfo)) { + return NS_ERROR_FAILURE; + } + + rv = platformInfo->GetPlatformBuildID(*gBuildId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) { + if (NS_WARN_IF(!gBasePath)) { + NS_WARNING("profile-do-change must precede profile-before-change-qm!"); + return NS_OK; + } + + // mPendingProfileChange is our re-entrancy guard (the nested event loop + // below may cause re-entrancy). + if (mPendingProfileChange) { + return NS_OK; + } + + AutoRestore<bool> pending(mPendingProfileChange); + mPendingProfileChange = true; + + mShutdownComplete = false; + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!backgroundActor->SendShutdownQuotaManager())) { + return NS_ERROR_FAILURE; + } + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "QuotaManager::Observer::Observe profile-before-change-qm"_ns, + [&]() { return mShutdownComplete; })); + + gBasePath = nullptr; + + gStorageName = nullptr; + + gBuildId = nullptr; + + Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, false); + + return NS_OK; + } + + if (!strcmp(aTopic, kPrivateBrowsingObserverTopic)) { + auto* const quotaManagerService = QuotaManagerService::GetOrCreate(); + if (NS_WARN_IF(!quotaManagerService)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIQuotaRequest> request; + rv = quotaManagerService->ClearStoragesForPrivateBrowsing( + nsGetterAddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + rv = Shutdown(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + if (!strcmp(aTopic, NS_WIDGET_WAKE_OBSERVER_TOPIC)) { + gLastOSWake = TimeStamp::Now(); + + return NS_OK; + } + + NS_WARNING("Unknown observer topic!"); + return NS_OK; +} + +/******************************************************************************* + * Quota manager + ******************************************************************************/ + +QuotaManager::QuotaManager(const nsAString& aBasePath, + const nsAString& aStorageName) + : mQuotaMutex("QuotaManager.mQuotaMutex"), + mBasePath(aBasePath), + mStorageName(aStorageName), + mTemporaryStorageUsage(0), + mNextDirectoryLockId(0), + mTemporaryStorageInitialized(false), + mCacheUsable(false), + mShuttingDownStorage(false) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!gInstance); +} + +QuotaManager::~QuotaManager() { + AssertIsOnOwningThread(); + MOZ_ASSERT(!gInstance || gInstance == this); +} + +// static +nsresult QuotaManager::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = Observer::Initialize(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult> +QuotaManager::GetOrCreate() { + AssertIsOnBackgroundThread(); + + if (gInstance) { + return WrapMovingNotNullUnchecked(RefPtr<QuotaManager>{gInstance}); + } + + QM_TRY(OkIf(gBasePath), Err(NS_ERROR_FAILURE), [](const auto&) { + NS_WARNING( + "Trying to create QuotaManager before profile-do-change! " + "Forgot to call do_get_profile()?"); + }); + + QM_TRY(OkIf(!IsShuttingDown()), Err(NS_ERROR_FAILURE), [](const auto&) { + MOZ_ASSERT(false, + "Trying to create QuotaManager after profile-before-change-qm!"); + }); + + auto instance = MakeRefPtr<QuotaManager>(*gBasePath, *gStorageName); + + QM_TRY(MOZ_TO_RESULT(instance->Init())); + + gInstance = instance; + + // Do this before clients have a chance to acquire a directory lock for the + // private repository. + gInstance->ClearPrivateRepository(); + + return WrapMovingNotNullUnchecked(std::move(instance)); +} + +Result<Ok, nsresult> QuotaManager::EnsureCreated() { + AssertIsOnBackgroundThread(); + + QM_TRY_RETURN(GetOrCreate().map([](const auto& res) { return Ok{}; })) +} + +// static +QuotaManager* QuotaManager::Get() { + // Does not return an owning reference. + return gInstance; +} + +// static +nsIObserver* QuotaManager::GetObserver() { + MOZ_ASSERT(NS_IsMainThread()); + + return Observer::GetInstance(); +} + +// static +bool QuotaManager::IsShuttingDown() { return gShutdown; } + +// static +void QuotaManager::ShutdownInstance() { + AssertIsOnBackgroundThread(); + + if (gInstance) { + gInstance->Shutdown(); + + gInstance = nullptr; + } else { + // If we were never initialized, just set the flag to avoid late creation. + gShutdown = true; + } + + RefPtr<Runnable> runnable = + NS_NewRunnableFunction("dom::quota::QuotaManager::ShutdownCompleted", + []() { Observer::ShutdownCompleted(); }); + MOZ_ASSERT(runnable); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); +} + +// static +void QuotaManager::Reset() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!gInstance); + MOZ_ASSERT(gShutdown); + + gShutdown = false; +} + +// static +bool QuotaManager::IsOSMetadata(const nsAString& aFileName) { + return aFileName.EqualsLiteral(DSSTORE_FILE_NAME) || + aFileName.EqualsLiteral(DESKTOP_FILE_NAME) || + aFileName.LowerCaseEqualsLiteral(DESKTOP_INI_FILE_NAME) || + aFileName.LowerCaseEqualsLiteral(THUMBS_DB_FILE_NAME); +} + +// static +bool QuotaManager::IsDotFile(const nsAString& aFileName) { + return aFileName.First() == char16_t('.'); +} + +void QuotaManager::RegisterDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mDirectoryLocks.AppendElement(WrapNotNullUnchecked(&aLock)); + + if (aLock.ShouldUpdateLockIdTable()) { + MutexAutoLock lock(mQuotaMutex); + + MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLockIdTable.Contains(aLock.Id())); + mDirectoryLockIdTable.InsertOrUpdate(aLock.Id(), + WrapNotNullUnchecked(&aLock)); + } + + if (aLock.ShouldUpdateLockTable()) { + DirectoryLockTable& directoryLockTable = + GetDirectoryLockTable(aLock.GetPersistenceType()); + + // XXX It seems that the contents of the array are never actually used, we + // just use that like an inefficient use counter. Can't we just change + // DirectoryLockTable to a nsTHashMap<nsCStringHashKey, uint32_t>? + directoryLockTable + .LookupOrInsertWith( + aLock.Origin(), + [this, &aLock] { + if (!IsShuttingDown()) { + UpdateOriginAccessTime(aLock.GetPersistenceType(), + aLock.OriginMetadata()); + } + return MakeUnique<nsTArray<NotNull<DirectoryLockImpl*>>>(); + }) + ->AppendElement(WrapNotNullUnchecked(&aLock)); + } + + aLock.SetRegistered(true); +} + +void QuotaManager::UnregisterDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + MOZ_ALWAYS_TRUE(mDirectoryLocks.RemoveElement(&aLock)); + + if (aLock.ShouldUpdateLockIdTable()) { + MutexAutoLock lock(mQuotaMutex); + + MOZ_DIAGNOSTIC_ASSERT(mDirectoryLockIdTable.Contains(aLock.Id())); + mDirectoryLockIdTable.Remove(aLock.Id()); + } + + if (aLock.ShouldUpdateLockTable()) { + DirectoryLockTable& directoryLockTable = + GetDirectoryLockTable(aLock.GetPersistenceType()); + + nsTArray<NotNull<DirectoryLockImpl*>>* array; + MOZ_ALWAYS_TRUE(directoryLockTable.Get(aLock.Origin(), &array)); + + MOZ_ALWAYS_TRUE(array->RemoveElement(&aLock)); + if (array->IsEmpty()) { + directoryLockTable.Remove(aLock.Origin()); + + if (!IsShuttingDown()) { + UpdateOriginAccessTime(aLock.GetPersistenceType(), + aLock.OriginMetadata()); + } + } + } + + aLock.SetRegistered(false); +} + +void QuotaManager::AddPendingDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + mPendingDirectoryLocks.AppendElement(&aLock); +} + +void QuotaManager::RemovePendingDirectoryLock(DirectoryLockImpl& aLock) { + AssertIsOnOwningThread(); + + MOZ_ALWAYS_TRUE(mPendingDirectoryLocks.RemoveElement(&aLock)); +} + +uint64_t QuotaManager::CollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLocks.IsEmpty()); + + // XXX This looks as if this could/should also use CollectLRUOriginInfosUntil, + // or maybe a generalization if that. + + struct MOZ_STACK_CLASS Helper final { + static void GetInactiveOriginInfos( + const nsTArray<NotNull<RefPtr<OriginInfo>>>& aOriginInfos, + const nsTArray<NotNull<const DirectoryLockImpl*>>& aLocks, + OriginInfosFlatTraversable& aInactiveOriginInfos) { + for (const auto& originInfo : aOriginInfos) { + MOZ_ASSERT(originInfo->mGroupInfo->mPersistenceType != + PERSISTENCE_TYPE_PERSISTENT); + + if (originInfo->LockedPersisted()) { + continue; + } + + // Never evict PERSISTENCE_TYPE_DEFAULT data associated to a + // moz-extension origin, unlike websites (which may more likely using + // the local data as a cache but still able to retrieve the same data + // from the server side) extensions do not have the same data stored + // anywhere else and evicting the data would result into potential data + // loss for the users. + // + // Also, unlike a website the extensions are explicitly installed and + // uninstalled by the user and all data associated to the extension + // principal will be completely removed once the addon is uninstalled. + if (originInfo->mGroupInfo->mPersistenceType != + PERSISTENCE_TYPE_TEMPORARY && + originInfo->IsExtensionOrigin()) { + continue; + } + + const auto originScope = OriginScope::FromOrigin(originInfo->mOrigin); + + const bool match = + std::any_of(aLocks.begin(), aLocks.end(), + [&originScope](const DirectoryLockImpl* const lock) { + return originScope.Matches(lock->GetOriginScope()); + }); + + if (!match) { + MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count(), + "Inactive origin shouldn't have open files!"); + aInactiveOriginInfos.InsertElementSorted( + originInfo, OriginInfoAccessTimeComparator()); + } + } + } + }; + + // Split locks into separate arrays and filter out locks for persistent + // storage, they can't block us. + auto [temporaryStorageLocks, defaultStorageLocks, + privateStorageLocks] = [this] { + nsTArray<NotNull<const DirectoryLockImpl*>> temporaryStorageLocks; + nsTArray<NotNull<const DirectoryLockImpl*>> defaultStorageLocks; + nsTArray<NotNull<const DirectoryLockImpl*>> privateStorageLocks; + + for (NotNull<const DirectoryLockImpl*> const lock : mDirectoryLocks) { + const Nullable<PersistenceType>& persistenceType = + lock->NullablePersistenceType(); + + if (persistenceType.IsNull()) { + temporaryStorageLocks.AppendElement(lock); + defaultStorageLocks.AppendElement(lock); + } else if (persistenceType.Value() == PERSISTENCE_TYPE_TEMPORARY) { + temporaryStorageLocks.AppendElement(lock); + } else if (persistenceType.Value() == PERSISTENCE_TYPE_DEFAULT) { + defaultStorageLocks.AppendElement(lock); + } else if (persistenceType.Value() == PERSISTENCE_TYPE_PRIVATE) { + privateStorageLocks.AppendElement(lock); + } else { + MOZ_ASSERT(persistenceType.Value() == PERSISTENCE_TYPE_PERSISTENT); + + // Do nothing here, persistent origins don't need to be collected ever. + } + } + + return std::make_tuple(std::move(temporaryStorageLocks), + std::move(defaultStorageLocks), + std::move(privateStorageLocks)); + }(); + + // Enumerate and process inactive origins. This must be protected by the + // mutex. + MutexAutoLock lock(mQuotaMutex); + + const auto [inactiveOrigins, sizeToBeFreed] = + [this, &temporaryStorageLocks = temporaryStorageLocks, + &defaultStorageLocks = defaultStorageLocks, + &privateStorageLocks = privateStorageLocks, aMinSizeToBeFreed] { + nsTArray<NotNull<RefPtr<const OriginInfo>>> inactiveOrigins; + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos, + temporaryStorageLocks, + inactiveOrigins); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + Helper::GetInactiveOriginInfos( + groupInfo->mOriginInfos, defaultStorageLocks, inactiveOrigins); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE); + if (groupInfo) { + Helper::GetInactiveOriginInfos( + groupInfo->mOriginInfos, privateStorageLocks, inactiveOrigins); + } + } + +#ifdef DEBUG + // Make sure the array is sorted correctly. + const bool inactiveOriginsSorted = + std::is_sorted(inactiveOrigins.cbegin(), inactiveOrigins.cend(), + [](const auto& lhs, const auto& rhs) { + return lhs->mAccessTime < rhs->mAccessTime; + }); + MOZ_ASSERT(inactiveOriginsSorted); +#endif + + // Create a list of inactive and the least recently used origins + // whose aggregate size is greater or equals the minimal size to be + // freed. + uint64_t sizeToBeFreed = 0; + for (uint32_t count = inactiveOrigins.Length(), index = 0; + index < count; index++) { + if (sizeToBeFreed >= aMinSizeToBeFreed) { + inactiveOrigins.TruncateLength(index); + break; + } + + sizeToBeFreed += inactiveOrigins[index]->LockedUsage(); + } + + return std::pair(std::move(inactiveOrigins), sizeToBeFreed); + }(); + + if (sizeToBeFreed >= aMinSizeToBeFreed) { + // Success, add directory locks for these origins, so any other + // operations for them will be delayed (until origin eviction is finalized). + + for (const auto& originInfo : inactiveOrigins) { + auto lock = DirectoryLockImpl::CreateForEviction( + WrapNotNullUnchecked(this), originInfo->mGroupInfo->mPersistenceType, + originInfo->FlattenToOriginMetadata()); + + lock->AcquireImmediately(); + + aLocks.AppendElement(lock.forget()); + } + + return sizeToBeFreed; + } + + return 0; +} + +template <typename P> +void QuotaManager::CollectPendingOriginsForListing(P aPredicate) { + MutexAutoLock lock(mQuotaMutex); + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + for (const auto& originInfo : groupInfo->mOriginInfos) { + if (!originInfo->mDirectoryExists) { + aPredicate(originInfo); + } + } + } + } +} + +nsresult QuotaManager::Init() { + AssertIsOnOwningThread(); + +#ifdef XP_WIN + CacheUseDOSDevicePathSyntaxPrefValue(); +#endif + + QM_TRY_INSPECT(const auto& baseDir, QM_NewLocalFile(mBasePath)); + + QM_TRY_UNWRAP( + do_Init(mIndexedDBPath), + GetPathForStorage(*baseDir, nsLiteralString(INDEXEDDB_DIRECTORY_NAME))); + + QM_TRY(MOZ_TO_RESULT(baseDir->Append(mStorageName))); + + QM_TRY_UNWRAP(do_Init(mStoragePath), + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, baseDir, GetPath)); + + QM_TRY_UNWRAP( + do_Init(mStorageArchivesPath), + GetPathForStorage(*baseDir, nsLiteralString(ARCHIVES_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mPermanentStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(PERMANENT_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mTemporaryStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(TEMPORARY_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mDefaultStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(DEFAULT_DIRECTORY_NAME))); + + QM_TRY_UNWRAP(do_Init(mPrivateStoragePath), + GetPathForStorage( + *baseDir, nsLiteralString(DEFAULT_PRIVATE_DIRECTORY_NAME))); + + QM_TRY_UNWRAP(do_Init(mIOThread), + MOZ_TO_RESULT_INVOKE_TYPED( + nsCOMPtr<nsIThread>, MOZ_SELECT_OVERLOAD(NS_NewNamedThread), + "QuotaManager IO")); + + static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 && + Client::FILESYSTEM == 3 && Client::LS == 4 && + Client::TYPE_MAX == 5, + "Fix the registration!"); + + // Register clients. + auto clients = decltype(mClients)::ValueType{}; + clients.AppendElement(indexedDB::CreateQuotaClient()); + clients.AppendElement(cache::CreateQuotaClient()); + clients.AppendElement(simpledb::CreateQuotaClient()); + clients.AppendElement(fs::CreateQuotaClient()); + if (NextGenLocalStorageEnabled()) { + clients.AppendElement(localstorage::CreateQuotaClient()); + } else { + clients.SetLength(Client::TypeMax()); + } + + mClients.init(std::move(clients)); + + MOZ_ASSERT(mClients->Capacity() == Client::TYPE_MAX, + "Should be using an auto array with correct capacity!"); + + mAllClientTypes.init(ClientTypesArray{ + Client::Type::IDB, Client::Type::DOMCACHE, Client::Type::SDB, + Client::Type::FILESYSTEM, Client::Type::LS}); + mAllClientTypesExceptLS.init( + ClientTypesArray{Client::Type::IDB, Client::Type::DOMCACHE, + Client::Type::SDB, Client::Type::FILESYSTEM}); + + return NS_OK; +} + +// static +void QuotaManager::MaybeRecordQuotaClientShutdownStep( + const Client::Type aClientType, const nsACString& aStepDescription) { + // Callable on any thread. + + auto* const quotaManager = QuotaManager::Get(); + MOZ_DIAGNOSTIC_ASSERT(quotaManager); + + if (quotaManager->IsShuttingDown()) { + quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription); + } +} + +// static +void QuotaManager::SafeMaybeRecordQuotaClientShutdownStep( + const Client::Type aClientType, const nsACString& aStepDescription) { + // Callable on any thread. + + auto* const quotaManager = QuotaManager::Get(); + + if (quotaManager && quotaManager->IsShuttingDown()) { + quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription); + } +} + +void QuotaManager::RecordQuotaManagerShutdownStep( + const nsACString& aStepDescription) { + // Callable on any thread. + MOZ_ASSERT(IsShuttingDown()); + + RecordShutdownStep(Nothing{}, aStepDescription); +} + +void QuotaManager::MaybeRecordQuotaManagerShutdownStep( + const nsACString& aStepDescription) { + // Callable on any thread. + + if (IsShuttingDown()) { + RecordQuotaManagerShutdownStep(aStepDescription); + } +} + +void QuotaManager::RecordShutdownStep(const Maybe<Client::Type> aClientType, + const nsACString& aStepDescription) { + MOZ_ASSERT(IsShuttingDown()); + + const TimeDuration elapsedSinceShutdownStart = + TimeStamp::NowLoRes() - *mShutdownStartedAt; + + const auto stepString = + nsPrintfCString("%fs: %s", elapsedSinceShutdownStart.ToSeconds(), + nsPromiseFlatCString(aStepDescription).get()); + + if (aClientType) { + AssertIsOnBackgroundThread(); + + mShutdownSteps[*aClientType].Append(stepString + "\n"_ns); + } else { + // Callable on any thread. + MutexAutoLock lock(mQuotaMutex); + + mQuotaManagerShutdownSteps.Append(stepString + "\n"_ns); + } + +#ifdef DEBUG + // XXX Probably this isn't the mechanism that should be used here. + + NS_DebugBreak( + NS_DEBUG_WARNING, + nsAutoCString(aClientType ? Client::TypeToText(*aClientType) + : "quota manager"_ns + " shutdown step"_ns) + .get(), + stepString.get(), __FILE__, __LINE__); +#endif +} + +void QuotaManager::Shutdown() { + AssertIsOnOwningThread(); + MOZ_DIAGNOSTIC_ASSERT(!gShutdown); + + // Define some local helper functions + + auto flagShutdownStarted = [this]() { + mShutdownStartedAt.init(TimeStamp::NowLoRes()); + + // Setting this flag prevents the service from being recreated and prevents + // further storages from being created. + gShutdown = true; + }; + + nsCOMPtr<nsITimer> crashBrowserTimer; + + auto crashBrowserTimerCallback = [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = static_cast<QuotaManager*>(aClosure); + + nsCString annotation; + + for (Client::Type type : quotaManager->AllClientTypes()) { + auto& quotaClient = *(*quotaManager->mClients)[type]; + + if (!quotaClient.IsShutdownCompleted()) { + annotation.AppendPrintf("%s: %s\nIntermediate steps:\n%s\n\n", + Client::TypeToText(type).get(), + quotaClient.GetShutdownStatus().get(), + quotaManager->mShutdownSteps[type].get()); + } + } + + if (gNormalOriginOps) { + annotation.AppendPrintf("QM: %zu normal origin ops pending\n", + gNormalOriginOps->Length()); +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + for (const auto& op : *gNormalOriginOps) { + nsCString name; + op->GetName(name); + annotation.AppendPrintf("Op: %s pending\n", name.get()); + } +#endif + } + { + MutexAutoLock lock(quotaManager->mQuotaMutex); + + annotation.AppendPrintf("Intermediate steps:\n%s\n", + quotaManager->mQuotaManagerShutdownSteps.get()); + } + + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::QuotaManagerShutdownTimeout, annotation); + + MOZ_CRASH("Quota manager shutdown timed out"); + }; + + auto startCrashBrowserTimer = [&]() { + crashBrowserTimer = NS_NewTimer(); + MOZ_ASSERT(crashBrowserTimer); + if (crashBrowserTimer) { + RecordQuotaManagerShutdownStep("startCrashBrowserTimer"_ns); + MOZ_ALWAYS_SUCCEEDS(crashBrowserTimer->InitWithNamedFuncCallback( + crashBrowserTimerCallback, this, SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::Shutdown::crashBrowserTimer")); + } + }; + + auto stopCrashBrowserTimer = [&]() { + if (crashBrowserTimer) { + RecordQuotaManagerShutdownStep("stopCrashBrowserTimer"_ns); + QM_WARNONLY_TRY(QM_TO_RESULT(crashBrowserTimer->Cancel())); + } + }; + + auto initiateShutdownWorkThreads = [this]() { + RecordQuotaManagerShutdownStep("initiateShutdownWorkThreads"_ns); + bool needsToWait = false; + for (Client::Type type : AllClientTypes()) { + // Clients are supposed to also AbortAllOperations from this point on + // to speed up shutdown, if possible. Thus pending operations + // might not be executed anymore. + needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads(); + } + + return needsToWait; + }; + + nsCOMPtr<nsITimer> killActorsTimer; + + auto killActorsTimerCallback = [](nsITimer* aTimer, void* aClosure) { + auto* const quotaManager = static_cast<QuotaManager*>(aClosure); + + quotaManager->RecordQuotaManagerShutdownStep("killActorsTimerCallback"_ns); + + // XXX: This abort is a workaround to unblock shutdown, which + // ought to be removed by bug 1682326. We probably need more + // checks to immediately abort new operations during + // shutdown. + quotaManager->GetClient(Client::IDB)->AbortAllOperations(); + + for (Client::Type type : quotaManager->AllClientTypes()) { + quotaManager->GetClient(type)->ForceKillActors(); + } + }; + + auto startKillActorsTimer = [&]() { + killActorsTimer = NS_NewTimer(); + MOZ_ASSERT(killActorsTimer); + if (killActorsTimer) { + RecordQuotaManagerShutdownStep("startKillActorsTimer"_ns); + MOZ_ALWAYS_SUCCEEDS(killActorsTimer->InitWithNamedFuncCallback( + killActorsTimerCallback, this, SHUTDOWN_KILL_ACTORS_TIMEOUT_MS, + nsITimer::TYPE_ONE_SHOT, + "quota::QuotaManager::Shutdown::killActorsTimer")); + } + }; + + auto stopKillActorsTimer = [&]() { + if (killActorsTimer) { + RecordQuotaManagerShutdownStep("stopKillActorsTimer"_ns); + QM_WARNONLY_TRY(QM_TO_RESULT(killActorsTimer->Cancel())); + } + }; + + auto isAllClientsShutdownComplete = [this] { + return std::all_of(AllClientTypes().cbegin(), AllClientTypes().cend(), + [&self = *this](const auto type) { + return (*self.mClients)[type]->IsShutdownCompleted(); + }); + }; + + auto shutdownAndJoinWorkThreads = [this]() { + RecordQuotaManagerShutdownStep("shutdownAndJoinWorkThreads"_ns); + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->FinalizeShutdownWorkThreads(); + } + }; + + auto shutdownAndJoinIOThread = [this]() { + RecordQuotaManagerShutdownStep("shutdownAndJoinIOThread"_ns); + + // Make sure to join with our IO thread. + QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown())); + }; + + auto invalidatePendingDirectoryLocks = [this]() { + RecordQuotaManagerShutdownStep("invalidatePendingDirectoryLocks"_ns); + for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) { + lock->Invalidate(); + } + }; + + // Body of the function + ScopedLogExtraInfo scope{ScopedLogExtraInfo::kTagContext, + "dom::quota::QuotaManager::Shutdown"_ns}; + + // This must be called before `flagShutdownStarted`, it would fail otherwise. + // `ShutdownStorageOp` needs to acquire an exclusive directory lock over + // entire <profile>/storage which will abort any existing operations and wait + // for all existing directory locks to be released. So the shutdown operation + // will effectively run after all existing operations. + // We don't need to use the returned promise here because `ShutdownStorage` + // registers `ShudownStorageOp` in `gNormalOriginOps`. + ShutdownStorage(); + + flagShutdownStarted(); + + startCrashBrowserTimer(); + + // XXX: StopIdleMaintenance now just notifies all clients to abort any + // maintenance work. + // This could be done as part of QuotaClient::AbortAllOperations. + StopIdleMaintenance(); + + // XXX In theory, we could simplify the code below (and also the `Client` + // interface) by removing the `initiateShutdownWorkThreads` and + // `isAllClientsShutdownComplete` calls because it should be sufficient + // to rely on `ShutdownStorage` to abort all existing operations and to + // wait for all existing directory locks to be released as well. + + const bool needsToWait = + initiateShutdownWorkThreads() || static_cast<bool>(gNormalOriginOps); + + // If any clients cannot shutdown immediately, spin the event loop while we + // wait on all the threads to close. + if (needsToWait) { + startKillActorsTimer(); + + MOZ_ALWAYS_TRUE(SpinEventLoopUntil( + "QuotaManager::Shutdown"_ns, [isAllClientsShutdownComplete]() { + return !gNormalOriginOps && isAllClientsShutdownComplete(); + })); + + stopKillActorsTimer(); + } + + shutdownAndJoinWorkThreads(); + + shutdownAndJoinIOThread(); + + invalidatePendingDirectoryLocks(); + + stopCrashBrowserTimer(); +} + +void QuotaManager::InitQuotaForOrigin( + const FullOriginMetadata& aFullOriginMetadata, + const ClientUsageArray& aClientUsages, uint64_t aUsageBytes) { + AssertIsOnIOThread(); + MOZ_ASSERT(IsBestEffortPersistenceType(aFullOriginMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo( + aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mSuffix, + aFullOriginMetadata.mGroup); + + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aFullOriginMetadata.mOrigin, + aFullOriginMetadata.mStorageOrigin, aFullOriginMetadata.mIsPrivate, + aClientUsages, aUsageBytes, aFullOriginMetadata.mLastAccessTime, + aFullOriginMetadata.mPersisted, + /* aDirectoryExists */ true)); +} + +void QuotaManager::EnsureQuotaForOrigin(const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo( + aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix, + aOriginMetadata.mGroup); + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (!originInfo) { + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aOriginMetadata.mOrigin, aOriginMetadata.mStorageOrigin, + aOriginMetadata.mIsPrivate, ClientUsageArray(), + /* aUsageBytes */ 0, + /* aAccessTime */ PR_Now(), /* aPersisted */ false, + /* aDirectoryExists */ false)); + } +} + +int64_t QuotaManager::NoteOriginDirectoryCreated( + const OriginMetadata& aOriginMetadata, bool aPersisted) { + AssertIsOnIOThread(); + MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType)); + + int64_t timestamp; + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo( + aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix, + aOriginMetadata.mGroup); + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (originInfo) { + timestamp = originInfo->LockedAccessTime(); + originInfo->mPersisted = aPersisted; + originInfo->mDirectoryExists = true; + } else { + timestamp = PR_Now(); + groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>( + groupInfo, aOriginMetadata.mOrigin, aOriginMetadata.mStorageOrigin, + aOriginMetadata.mIsPrivate, ClientUsageArray(), + /* aUsageBytes */ 0, + /* aAccessTime */ timestamp, aPersisted, /* aDirectoryExists */ true)); + } + + return timestamp; +} + +void QuotaManager::DecreaseUsageForClient(const ClientMetadata& aClientMetadata, + int64_t aSize) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin); + if (originInfo) { + originInfo->LockedDecreaseUsage(aClientMetadata.mClientType, aSize); + } +} + +void QuotaManager::ResetUsageForClient(const ClientMetadata& aClientMetadata) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType)); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin); + if (originInfo) { + originInfo->LockedResetUsageForClient(aClientMetadata.mClientType); + } +} + +UsageInfo QuotaManager::GetUsageForClient(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + Client::Type aClientType) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return UsageInfo{}; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return UsageInfo{}; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (!originInfo) { + return UsageInfo{}; + } + + return originInfo->LockedGetUsageForClient(aClientType); +} + +void QuotaManager::UpdateOriginAccessTime( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); + MOZ_ASSERT(!IsShuttingDown()); + + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + return; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + if (originInfo) { + int64_t timestamp = PR_Now(); + originInfo->LockedUpdateAccessTime(timestamp); + + MutexAutoUnlock autoUnlock(mQuotaMutex); + + auto op = MakeRefPtr<SaveOriginAccessTimeOp>(aOriginMetadata, timestamp); + + RegisterNormalOriginOp(*op); + + op->RunImmediately(); + } +} + +void QuotaManager::RemoveQuota() { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + + groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE); + if (groupInfo) { + groupInfo->LockedRemoveOriginInfos(); + } + } + + mGroupInfoPairs.Clear(); + + MOZ_ASSERT(mTemporaryStorageUsage == 0, "Should be zero!"); +} + +nsresult QuotaManager::LoadQuota() { + AssertIsOnIOThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(!mTemporaryStorageInitialized); + + // A list of all unaccessed default or temporary origins. + nsTArray<FullOriginMetadata> unaccessedOrigins; + + auto MaybeCollectUnaccessedOrigin = + [loadQuotaInfoStartTime = PR_Now(), + &unaccessedOrigins](auto& fullOriginMetadata) { + if (IsOriginUnaccessed(fullOriginMetadata, loadQuotaInfoStartTime)) { + unaccessedOrigins.AppendElement(std::move(fullOriginMetadata)); + } + }; + + auto recordQuotaInfoLoadTimeHelper = + MakeRefPtr<RecordQuotaInfoLoadTimeHelper>(); + + const auto startTime = recordQuotaInfoLoadTimeHelper->Start(); + + auto LoadQuotaFromCache = [&]() -> nsresult { + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement, + "SELECT repository_id, suffix, group_, " + "origin, client_usages, usage, " + "last_access_time, accessed, persisted " + "FROM origin"_ns)); + + auto autoRemoveQuota = MakeScopeExit([&] { + RemoveQuota(); + unaccessedOrigins.Clear(); + }); + + QM_TRY(quota::CollectWhileHasResult( + *stmt, + [this, + &MaybeCollectUnaccessedOrigin](auto& stmt) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const int32_t& repositoryId, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); + + const auto maybePersistenceType = + PersistenceTypeFromInt32(repositoryId, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + FullOriginMetadata fullOriginMetadata; + + fullOriginMetadata.mPersistenceType = maybePersistenceType.value(); + + QM_TRY_UNWRAP(fullOriginMetadata.mSuffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 1)); + + QM_TRY_UNWRAP(fullOriginMetadata.mGroup, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 2)); + + QM_TRY_UNWRAP(fullOriginMetadata.mOrigin, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 3)); + + fullOriginMetadata.mStorageOrigin = fullOriginMetadata.mOrigin; + + fullOriginMetadata.mIsPrivate = false; + + QM_TRY_INSPECT(const auto& clientUsagesText, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt, + GetUTF8String, 4)); + + ClientUsageArray clientUsages; + QM_TRY(MOZ_TO_RESULT(clientUsages.Deserialize(clientUsagesText))); + + QM_TRY_INSPECT(const int64_t& usage, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 5)); + QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 6)); + QM_TRY_INSPECT(const int64_t& accessed, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 7)); + QM_TRY_UNWRAP(fullOriginMetadata.mPersisted, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 8)); + + QM_TRY_INSPECT(const bool& groupUpdated, + MaybeUpdateGroupForOrigin(fullOriginMetadata)); + + Unused << groupUpdated; + + QM_TRY_INSPECT( + const bool& lastAccessTimeUpdated, + MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata)); + + Unused << lastAccessTimeUpdated; + + // We don't need to update the .metadata-v2 file on disk here, + // EnsureTemporaryOriginIsInitialized is responsible for doing that. + // We just need to use correct group and last access time before + // initializing quota for the given origin. (Note that calling + // LoadFullOriginMetadataWithRestore below might update the group in + // the metadata file, but only as a side-effect. The actual place we + // ensure consistency is in EnsureTemporaryOriginIsInitialized.) + + if (accessed) { + QM_TRY_INSPECT(const auto& directory, + GetOriginDirectory(fullOriginMetadata)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE)); + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE)); + + // Calling LoadFullOriginMetadataWithRestore might update the group + // in the metadata file, but only as a side-effect. The actual place + // we ensure consistency is in EnsureTemporaryOriginIsInitialized. + + QM_TRY_INSPECT(const auto& metadata, + LoadFullOriginMetadataWithRestore(directory)); + + QM_TRY(OkIf(fullOriginMetadata.mLastAccessTime == + metadata.mLastAccessTime), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mPersisted == metadata.mPersisted), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mPersistenceType == + metadata.mPersistenceType), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mSuffix == metadata.mSuffix), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mGroup == metadata.mGroup), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mOrigin == metadata.mOrigin), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mStorageOrigin == + metadata.mStorageOrigin), + Err(NS_ERROR_FAILURE)); + + QM_TRY(OkIf(fullOriginMetadata.mIsPrivate == metadata.mIsPrivate), + Err(NS_ERROR_FAILURE)); + + QM_TRY(MOZ_TO_RESULT(InitializeOrigin( + fullOriginMetadata.mPersistenceType, fullOriginMetadata, + fullOriginMetadata.mLastAccessTime, + fullOriginMetadata.mPersisted, directory))); + } else { + InitQuotaForOrigin(fullOriginMetadata, clientUsages, usage); + } + + MaybeCollectUnaccessedOrigin(fullOriginMetadata); + + return Ok{}; + })); + + autoRemoveQuota.release(); + + return NS_OK; + }; + + QM_TRY_INSPECT( + const bool& loadQuotaFromCache, ([this]() -> Result<bool, nsresult> { + if (mCacheUsable) { + QM_TRY_INSPECT( + const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *mStorageConnection, "SELECT valid, build_id FROM cache"_ns)); + + QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_INSPECT(const int32_t& valid, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0)); + + if (valid) { + if (!StaticPrefs::dom_quotaManager_caching_checkBuildId()) { + return true; + } + + QM_TRY_INSPECT(const auto& buildId, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoCString, stmt, GetUTF8String, 1)); + + return buildId == *gBuildId; + } + } + + return false; + }())); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + if (!loadQuotaFromCache || + !StaticPrefs::dom_quotaManager_loadQuotaFromCache() || + ![&LoadQuotaFromCache] { + QM_WARNONLY_TRY_UNWRAP(auto res, MOZ_TO_RESULT(LoadQuotaFromCache())); + return static_cast<bool>(res); + }()) { + // A keeper to defer the return only in Nightly, so that the telemetry data + // for whole profile can be collected. +#ifdef NIGHTLY_BUILD + nsresult statusKeeper = NS_OK; +#endif + + const auto statusKeeperFunc = [&](const nsresult rv) { + RECORD_IN_NIGHTLY(statusKeeper, rv); + }; + + for (const PersistenceType type : + kInitializableBestEffortPersistenceTypes) { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT(([this, type, &MaybeCollectUnaccessedOrigin] { + const auto innerFunc = [&](const auto&) -> nsresult { + return InitializeRepository(type, + MaybeCollectUnaccessedOrigin); + }; + + return ExecuteInitialization( + type == PERSISTENCE_TYPE_DEFAULT + ? Initialization::DefaultRepository + : Initialization::TemporaryRepository, + innerFunc); + }())), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + } + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + } + + autoRemoveQuota.release(); + + const auto endTime = recordQuotaInfoLoadTimeHelper->End(); + + if (StaticPrefs::dom_quotaManager_checkQuotaInfoLoadTime() && + static_cast<uint32_t>((endTime - startTime).ToMilliseconds()) >= + StaticPrefs::dom_quotaManager_longQuotaInfoLoadTimeThresholdMs() && + !unaccessedOrigins.IsEmpty()) { + QM_WARNONLY_TRY(ArchiveOrigins(unaccessedOrigins)); + } + + return NS_OK; +} + +void QuotaManager::UnloadQuota() { + AssertIsOnIOThread(); + MOZ_ASSERT(mStorageConnection); + MOZ_ASSERT(mTemporaryStorageInitialized); + MOZ_ASSERT(mCacheUsable); + + auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); }); + + mozStorageTransaction transaction( + mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start()), QM_VOID); + + QM_TRY(MOZ_TO_RESULT( + mStorageConnection->ExecuteSimpleSQL("DELETE FROM origin;"_ns)), + QM_VOID); + + nsCOMPtr<mozIStorageStatement> insertStmt; + + { + MutexAutoLock lock(mQuotaMutex); + + for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(!iter.Key().IsEmpty()); + + GroupInfoPair* const pair = iter.UserData(); + MOZ_ASSERT(pair); + + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (!groupInfo) { + continue; + } + + for (const auto& originInfo : groupInfo->mOriginInfos) { + MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count()); + + if (!originInfo->mDirectoryExists) { + continue; + } + + if (originInfo->mIsPrivate) { + continue; + } + + if (insertStmt) { + MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset()); + } else { + QM_TRY_UNWRAP( + insertStmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, + CreateStatement, + "INSERT INTO origin (repository_id, suffix, group_, " + "origin, client_usages, usage, last_access_time, " + "accessed, persisted) " + "VALUES (:repository_id, :suffix, :group_, :origin, " + ":client_usages, :usage, :last_access_time, :accessed, " + ":persisted)"_ns), + QM_VOID); + } + + QM_TRY(MOZ_TO_RESULT(originInfo->LockedBindToStatement(insertStmt)), + QM_VOID); + + QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()), QM_VOID); + } + + groupInfo->LockedRemoveOriginInfos(); + } + + iter.Remove(); + } + } + + QM_TRY_INSPECT( + const auto& stmt, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement, + "UPDATE cache SET valid = :valid, build_id = :buildId;"_ns), + QM_VOID); + + QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("valid"_ns, 1)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("buildId"_ns, *gBuildId)), + QM_VOID); + QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_VOID); + QM_TRY(MOZ_TO_RESULT(transaction.Commit()), QM_VOID); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, nsIFile* aFile, int64_t aFileSize, + int64_t* aFileSizeOut /* = nullptr */) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); + + if (aFileSizeOut) { + *aFileSizeOut = 0; + } + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + return nullptr; + } + + QM_TRY_INSPECT(const auto& path, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aFile, GetPath), + nullptr); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata), + nullptr); + + nsAutoString clientType; + QM_TRY(OkIf(Client::TypeToText(aClientType, clientType, fallible)), + nullptr); + + QM_TRY(MOZ_TO_RESULT(directory->Append(clientType)), nullptr); + + QM_TRY_INSPECT( + const auto& directoryPath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directory, GetPath), + nullptr); + + MOZ_ASSERT(StringBeginsWith(path, directoryPath)); + } +#endif + + QM_TRY_INSPECT( + const int64_t fileSize, + ([&aFile, aFileSize]() -> Result<int64_t, nsresult> { + if (aFileSize == -1) { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists)); + + if (exists) { + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize)); + } + + return 0; + } + + return aFileSize; + }()), + nullptr); + + RefPtr<QuotaObject> result; + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return nullptr; + } + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + + if (!groupInfo) { + return nullptr; + } + + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + + if (!originInfo) { + return nullptr; + } + + // We need this extra raw pointer because we can't assign to the smart + // pointer directly since QuotaObject::AddRef would try to acquire the same + // mutex. + const NotNull<CanonicalQuotaObject*> canonicalQuotaObject = + originInfo->mCanonicalQuotaObjects.LookupOrInsertWith(path, [&] { + // Create a new QuotaObject. The hashtable is not responsible to + // delete the QuotaObject. + return WrapNotNullUnchecked(new CanonicalQuotaObject( + originInfo, aClientType, path, fileSize)); + }); + + // Addref the QuotaObject and move the ownership to the result. This must + // happen before we unlock! + result = canonicalQuotaObject->LockedAddRef(); + } + + if (aFileSizeOut) { + *aFileSizeOut = fileSize; + } + + // The caller becomes the owner of the QuotaObject, that is, the caller is + // is responsible to delete it when the last reference is removed. + return result.forget(); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, const nsAString& aPath, int64_t aFileSize, + int64_t* aFileSizeOut /* = nullptr */) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + if (aFileSizeOut) { + *aFileSizeOut = 0; + } + + QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath), nullptr); + + return GetQuotaObject(aPersistenceType, aOriginMetadata, aClientType, file, + aFileSize, aFileSizeOut); +} + +already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject( + const int64_t aDirectoryLockId, const nsAString& aPath) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + Maybe<MutexAutoLock> lock; + + // See the comment for mDirectoryLockIdTable in QuotaManager.h + if (!IsOnBackgroundThread()) { + lock.emplace(mQuotaMutex); + } + + if (auto maybeDirectoryLock = + mDirectoryLockIdTable.MaybeGet(aDirectoryLockId)) { + const auto& directoryLock = *maybeDirectoryLock; + MOZ_DIAGNOSTIC_ASSERT(directoryLock->ShouldUpdateLockIdTable()); + + const PersistenceType persistenceType = directoryLock->GetPersistenceType(); + const OriginMetadata& originMetadata = directoryLock->OriginMetadata(); + const Client::Type clientType = directoryLock->ClientType(); + + lock.reset(); + + return GetQuotaObject(persistenceType, originMetadata, clientType, aPath); + } + + MOZ_CRASH("Getting quota object for an unregistered directory lock?"); +} + +Nullable<bool> QuotaManager::OriginPersisted( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata); + if (originInfo) { + return Nullable<bool>(originInfo->LockedPersisted()); + } + + return Nullable<bool>(); +} + +void QuotaManager::PersistOrigin(const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata); + if (originInfo && !originInfo->LockedPersisted()) { + originInfo->LockedPersist(); + } +} + +void QuotaManager::AbortOperationsForLocks( + const DirectoryLockIdTableArray& aLockIds) { + for (Client::Type type : AllClientTypes()) { + if (aLockIds[type].Filled()) { + (*mClients)[type]->AbortOperationsForLocks(aLockIds[type]); + } + } +} + +void QuotaManager::AbortOperationsForProcess(ContentParentId aContentParentId) { + AssertIsOnOwningThread(); + + for (const RefPtr<Client>& client : *mClients) { + client->AbortOperationsForProcess(aContentParentId); + } +} + +Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetOriginDirectory( + const OriginMetadata& aOriginMetadata) const { + QM_TRY_UNWRAP( + auto directory, + QM_NewLocalFile(GetStoragePath(aOriginMetadata.mPersistenceType))); + + QM_TRY(MOZ_TO_RESULT(directory->Append( + MakeSanitizedOriginString(aOriginMetadata.mStorageOrigin)))); + + return directory; +} + +nsresult QuotaManager::RestoreDirectoryMetadata2(nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + RefPtr<RestoreDirectoryMetadata2Helper> helper = + new RestoreDirectoryMetadata2Helper(aDirectory); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->RestoreMetadata2File())); + + return NS_OK; +} + +Result<FullOriginMetadata, nsresult> QuotaManager::LoadFullOriginMetadata( + nsIFile* aDirectory, PersistenceType aPersistenceType) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + QM_TRY_INSPECT(const auto& binaryStream, + GetBinaryInputStream(*aDirectory, + nsLiteralString(METADATA_V2_FILE_NAME))); + + FullOriginMetadata fullOriginMetadata; + + QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + QM_TRY_UNWRAP(fullOriginMetadata.mPersisted, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + + QM_TRY_INSPECT(const bool& reservedData1, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData1; + + // XXX Use for the persistence type. + QM_TRY_INSPECT(const bool& reservedData2, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData2; + + fullOriginMetadata.mPersistenceType = aPersistenceType; + + QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + Unused << suffix; + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + Unused << group; + + QM_TRY_UNWRAP( + fullOriginMetadata.mStorageOrigin, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString)); + + // Currently used for isPrivate (used to be used for isApp). + QM_TRY_UNWRAP(fullOriginMetadata.mIsPrivate, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + + QM_TRY(MOZ_TO_RESULT(binaryStream->Close())); + + auto principal = + [&storageOrigin = + fullOriginMetadata.mStorageOrigin]() -> nsCOMPtr<nsIPrincipal> { + if (storageOrigin.EqualsLiteral(kChromeOrigin)) { + return SystemPrincipal::Get(); + } + return BasePrincipal::CreateContentPrincipal(storageOrigin); + }(); + QM_TRY(MOZ_TO_RESULT(principal)); + + PrincipalInfo principalInfo; + QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo))); + + QM_TRY(MOZ_TO_RESULT(IsPrincipalInfoValid(principalInfo)), + Err(NS_ERROR_MALFORMED_URI)); + + QM_TRY_UNWRAP(auto principalMetadata, + GetInfoFromValidatedPrincipalInfo(principalInfo)); + + fullOriginMetadata.mSuffix = std::move(principalMetadata.mSuffix); + fullOriginMetadata.mGroup = std::move(principalMetadata.mGroup); + fullOriginMetadata.mOrigin = std::move(principalMetadata.mOrigin); + + QM_TRY_INSPECT(const bool& groupUpdated, + MaybeUpdateGroupForOrigin(fullOriginMetadata)); + + // A workaround for a bug in GetLastModifiedTime implementation which should + // have returned the current time instead of INT64_MIN when there were no + // suitable files for getting last modified time. + QM_TRY_INSPECT(const bool& lastAccessTimeUpdated, + MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata)); + + if (groupUpdated || lastAccessTimeUpdated) { + // Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce + // I/O. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aDirectory, fullOriginMetadata.mLastAccessTime, + fullOriginMetadata.mPersisted, fullOriginMetadata))); + } + + return fullOriginMetadata; +} + +Result<FullOriginMetadata, nsresult> +QuotaManager::LoadFullOriginMetadataWithRestore(nsIFile* aDirectory) { + // XXX Once the persistence type is stored in the metadata file, this block + // for getting the persistence type from the parent directory name can be + // removed. + nsCOMPtr<nsIFile> parentDir; + QM_TRY(MOZ_TO_RESULT(aDirectory->GetParent(getter_AddRefs(parentDir)))); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*parentDir, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + const auto& persistenceType = maybePersistenceType.value(); + + QM_TRY_RETURN(QM_OR_ELSE_WARN( + // Expression. + LoadFullOriginMetadata(aDirectory, persistenceType), + // Fallback. + ([&aDirectory, &persistenceType, + this](const nsresult rv) -> Result<FullOriginMetadata, nsresult> { + QM_TRY(MOZ_TO_RESULT(RestoreDirectoryMetadata2(aDirectory))); + + QM_TRY_RETURN(LoadFullOriginMetadata(aDirectory, persistenceType)); + }))); +} + +Result<OriginMetadata, nsresult> QuotaManager::GetOriginMetadata( + nsIFile* aDirectory) { + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(mStorageConnection); + + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aDirectory, GetLeafName)); + + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + OriginParser::ResultType result = OriginParser::ParseOrigin( + NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix); + QM_TRY(MOZ_TO_RESULT(result == OriginParser::ValidOrigin)); + + QM_TRY_INSPECT( + const auto& principal, + ([&spec, &attrs]() -> Result<nsCOMPtr<nsIPrincipal>, nsresult> { + if (spec.EqualsLiteral(kChromeOrigin)) { + return nsCOMPtr<nsIPrincipal>(SystemPrincipal::Get()); + } + + nsCOMPtr<nsIURI> uri; + QM_TRY(MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), spec))); + + return nsCOMPtr<nsIPrincipal>( + BasePrincipal::CreateContentPrincipal(uri, attrs)); + }())); + QM_TRY(MOZ_TO_RESULT(principal)); + + PrincipalInfo principalInfo; + QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo))); + + QM_TRY(MOZ_TO_RESULT(IsPrincipalInfoValid(principalInfo)), + Err(NS_ERROR_MALFORMED_URI)); + + QM_TRY_UNWRAP(auto principalMetadata, + GetInfoFromValidatedPrincipalInfo(principalInfo)); + + QM_TRY_INSPECT(const auto& parentDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>, + aDirectory, GetParent)); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*parentDirectory, fallible); + QM_TRY(MOZ_TO_RESULT(maybePersistenceType.isSome())); + + return OriginMetadata{std::move(principalMetadata), + maybePersistenceType.value()}; +} + +template <typename OriginFunc> +nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType, + OriginFunc&& aOriginFunc) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY || + aPersistenceType == PERSISTENCE_TYPE_DEFAULT); + + QM_TRY_INSPECT(const auto& directory, + QM_NewLocalFile(GetStoragePath(aPersistenceType))); + + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*directory)); + + Unused << created; + + // A keeper to defer the return only in Nightly, so that the telemetry data + // for whole profile can be collected +#ifdef NIGHTLY_BUILD + nsresult statusKeeper = NS_OK; +#endif + + const auto statusKeeperFunc = [&](const nsresult rv) { + RECORD_IN_NIGHTLY(statusKeeper, rv); + }; + + struct RenameAndInitInfo { + nsCOMPtr<nsIFile> mOriginDirectory; + FullOriginMetadata mFullOriginMetadata; + int64_t mTimestamp; + bool mPersisted; + }; + nsTArray<RenameAndInitInfo> renameAndInitInfos; + + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY( + CollectEachFile( + *directory, + [&](nsCOMPtr<nsIFile>&& childDirectory) -> Result<Ok, nsresult> { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY( + ([this, &childDirectory, &renameAndInitInfos, + aPersistenceType, &aOriginFunc]() -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, childDirectory, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, + GetDirEntryKind(*childDirectory)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + QM_TRY_UNWRAP( + auto maybeMetadata, + QM_OR_ELSE_WARN_IF( + // Expression + LoadFullOriginMetadataWithRestore( + childDirectory) + .map([](auto metadata) + -> Maybe<FullOriginMetadata> { + return Some(std::move(metadata)); + }), + // Predicate. + IsSpecificError<NS_ERROR_MALFORMED_URI>, + // Fallback. + ErrToDefaultOk<Maybe<FullOriginMetadata>>)); + + if (!maybeMetadata) { + // Unknown directories during initialization are + // allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + } + + auto metadata = maybeMetadata.extract(); + + MOZ_ASSERT(metadata.mPersistenceType == + aPersistenceType); + + // FIXME(tt): The check for origin name consistency can + // be removed once we have an upgrade to traverse origin + // directories and check through the directory metadata + // files. + const auto originSanitized = + MakeSanitizedOriginCString(metadata.mOrigin); + + NS_ConvertUTF16toUTF8 utf8LeafName(leafName); + if (!originSanitized.Equals(utf8LeafName)) { + QM_WARNING( + "The name of the origin directory (%s) doesn't " + "match the sanitized origin string (%s) in the " + "metadata file!", + utf8LeafName.get(), originSanitized.get()); + + // If it's the known case, we try to restore the + // origin directory name if it's possible. + if (originSanitized.Equals(utf8LeafName + "."_ns)) { + const int64_t lastAccessTime = + metadata.mLastAccessTime; + const bool persisted = metadata.mPersisted; + renameAndInitInfos.AppendElement(RenameAndInitInfo{ + std::move(childDirectory), std::move(metadata), + lastAccessTime, persisted}); + break; + } + + // XXXtt: Try to restore the unknown cases base on the + // content for their metadata files. Note that if the + // restore fails, QM should maintain a list and ensure + // they won't be accessed after initialization. + } + + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(InitializeOrigin( + aPersistenceType, metadata, + metadata.mLastAccessTime, metadata.mPersisted, + childDirectory)), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ([&childDirectory]( + const nsresult rv) -> Result<Ok, nsresult> { + // If the origin can't be initialized due to + // corruption, this is a permanent + // condition, and we need to remove all data + // for the origin on disk. + + QM_TRY( + MOZ_TO_RESULT(childDirectory->Remove(true))); + + return Ok{}; + }))); + + std::forward<OriginFunc>(aOriginFunc)(metadata); + + break; + } + + case nsIFileKind::ExistsAsFile: + if (IsOSMetadata(leafName) || IsDotFile(leafName)) { + break; + } + + // Unknown files during initialization are now allowed. + // Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while + // iterating. + break; + } + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + + for (auto& info : renameAndInitInfos) { + QM_TRY(([&]() -> Result<Ok, nsresult> { + QM_TRY(([&directory, &info, this, aPersistenceType, + &aOriginFunc]() -> Result<Ok, nsresult> { + const auto originDirName = + MakeSanitizedOriginString(info.mFullOriginMetadata.mOrigin); + + // Check if targetDirectory exist. + QM_TRY_INSPECT(const auto& targetDirectory, + CloneFileAndAppend(*directory, originDirName)); + + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER( + targetDirectory, Exists)); + + if (exists) { + QM_TRY(MOZ_TO_RESULT(info.mOriginDirectory->Remove(true))); + + return Ok{}; + } + + QM_TRY(MOZ_TO_RESULT( + info.mOriginDirectory->RenameTo(nullptr, originDirName))); + + // XXX We don't check corruption here ? + QM_TRY(MOZ_TO_RESULT(InitializeOrigin( + aPersistenceType, info.mFullOriginMetadata, info.mTimestamp, + info.mPersisted, targetDirectory))); + + std::forward<OriginFunc>(aOriginFunc)(info.mFullOriginMetadata); + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + } + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + + return NS_OK; +} + +nsresult QuotaManager::InitializeOrigin(PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, + int64_t aAccessTime, bool aPersisted, + nsIFile* aDirectory) { + AssertIsOnIOThread(); + + const bool trackQuota = aPersistenceType != PERSISTENCE_TYPE_PERSISTENT; + + // We need to initialize directories of all clients if they exists and also + // get the total usage to initialize the quota. + + ClientUsageArray clientUsages; + + // A keeper to defer the return only in Nightly, so that the telemetry data + // for whole profile can be collected +#ifdef NIGHTLY_BUILD + nsresult statusKeeper = NS_OK; +#endif + + QM_TRY(([&, statusKeeperFunc = [&](const nsresult rv) { + RECORD_IN_NIGHTLY(statusKeeper, rv); + }]() -> Result<Ok, nsresult> { + QM_TRY( + CollectEachFile( + *aDirectory, + [&](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + if (NS_WARN_IF(IsShuttingDown())) { + RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT); + } + + QM_TRY( + ([this, &file, trackQuota, aPersistenceType, &aOriginMetadata, + &clientUsages]() -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, file, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, + GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + Client::Type clientType; + const bool ok = Client::TypeFromText( + leafName, clientType, fallible); + if (!ok) { + // Unknown directories during initialization are now + // allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + } + + if (trackQuota) { + QM_TRY_INSPECT( + const auto& usageInfo, + (*mClients)[clientType]->InitOrigin( + aPersistenceType, aOriginMetadata, + /* aCanceled */ Atomic<bool>(false))); + + MOZ_ASSERT(!clientUsages[clientType]); + + if (usageInfo.TotalUsage()) { + // XXX(Bug 1683863) Until we identify the root cause + // of seemingly converted-from-negative usage + // values, we will just treat them as unset here, + // but log a warning to the browser console. + if (static_cast<int64_t>(*usageInfo.TotalUsage()) >= + 0) { + clientUsages[clientType] = usageInfo.TotalUsage(); + } else { +#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG) + const nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (console) { + console->LogStringMessage( + nsString( + u"QuotaManager warning: client "_ns + + leafName + + u" reported negative usage for group "_ns + + NS_ConvertUTF8toUTF16( + aOriginMetadata.mGroup) + + u", origin "_ns + + NS_ConvertUTF8toUTF16( + aOriginMetadata.mOrigin)) + .get()); + } +#endif + } + } + } else { + QM_TRY(MOZ_TO_RESULT( + (*mClients)[clientType] + ->InitOriginWithoutTracking( + aPersistenceType, aOriginMetadata, + /* aCanceled */ Atomic<bool>(false)))); + } + + break; + } + + case nsIFileKind::ExistsAsFile: + if (IsOriginMetadata(leafName)) { + break; + } + + if (IsTempMetadata(leafName)) { + QM_TRY(MOZ_TO_RESULT( + file->Remove(/* recursive */ false))); + + break; + } + + if (IsOSMetadata(leafName) || IsDotFile(leafName)) { + break; + } + + // Unknown files during initialization are now allowed. + // Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + // Bug 1595448 will handle the case for unknown files + // like idb, cache, or ls. + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while + // iterating. + break; + } + + return Ok{}; + }()), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }), + OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc); + + return Ok{}; + }())); + +#ifdef NIGHTLY_BUILD + if (NS_FAILED(statusKeeper)) { + return statusKeeper; + } +#endif + + if (trackQuota) { + const auto usage = std::accumulate( + clientUsages.cbegin(), clientUsages.cend(), CheckedUint64(0), + [](CheckedUint64 value, const Maybe<uint64_t>& clientUsage) { + return value + clientUsage.valueOr(0); + }); + + // XXX Should we log more information, i.e. the whole clientUsages array, in + // case usage is not valid? + + QM_TRY(OkIf(usage.isValid()), NS_ERROR_FAILURE); + + InitQuotaForOrigin( + FullOriginMetadata{aOriginMetadata, aPersisted, aAccessTime}, + clientUsages, usage.value()); + } + + return NS_OK; +} + +nsresult +QuotaManager::UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + nsIFile* aIndexedDBDir) { + AssertIsOnIOThread(); + MOZ_ASSERT(aIndexedDBDir); + + const auto innerFunc = [this, &aIndexedDBDir](const auto&) -> nsresult { + bool isDirectory; + QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->IsDirectory(&isDirectory))); + + if (!isDirectory) { + NS_WARNING("indexedDB entry is not a directory!"); + return NS_OK; + } + + auto persistentStorageDirOrErr = QM_NewLocalFile(*mStoragePath); + if (NS_WARN_IF(persistentStorageDirOrErr.isErr())) { + return persistentStorageDirOrErr.unwrapErr(); + } + + nsCOMPtr<nsIFile> persistentStorageDir = persistentStorageDirOrErr.unwrap(); + + QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append( + nsLiteralString(PERSISTENT_DIRECTORY_NAME)))); + + bool exists; + QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Exists(&exists))); + + if (exists) { + QM_WARNING("Deleting old <profile>/indexedDB directory!"); + + QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->Remove(/* aRecursive */ true))); + + return NS_OK; + } + + nsCOMPtr<nsIFile> storageDir; + QM_TRY(MOZ_TO_RESULT( + persistentStorageDir->GetParent(getter_AddRefs(storageDir)))); + + // MoveTo() is atomic if the move happens on the same volume which should + // be our case, so even if we crash in the middle of the operation nothing + // breaks next time we try to initialize. + // However there's a theoretical possibility that the indexedDB directory + // is on different volume, but it should be rare enough that we don't have + // to worry about it. + QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->MoveTo( + storageDir, nsLiteralString(PERSISTENT_DIRECTORY_NAME)))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeFromIndexedDBDirectory, + innerFunc); +} + +nsresult +QuotaManager::UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + nsIFile* aPersistentStorageDir) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistentStorageDir); + + const auto innerFunc = [this, + &aPersistentStorageDir](const auto&) -> nsresult { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aPersistentStorageDir, IsDirectory)); + + if (!isDirectory) { + NS_WARNING("persistent entry is not a directory!"); + return NS_OK; + } + + { + QM_TRY_INSPECT(const auto& defaultStorageDir, + QM_NewLocalFile(*mDefaultStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists)); + + if (exists) { + QM_WARNING("Deleting old <profile>/storage/persistent directory!"); + + QM_TRY(MOZ_TO_RESULT( + aPersistentStorageDir->Remove(/* aRecursive */ true))); + + return NS_OK; + } + } + + { + // Create real metadata files for origin directories in persistent + // storage. + auto helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>( + aPersistentStorageDir); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository())); + + // Upgrade metadata files for origin directories in temporary storage. + QM_TRY_INSPECT(const auto& temporaryStorageDir, + QM_NewLocalFile(*mTemporaryStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, Exists)); + + if (exists) { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, IsDirectory)); + + if (!isDirectory) { + NS_WARNING("temporary entry is not a directory!"); + return NS_OK; + } + + helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>( + temporaryStorageDir); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository())); + } + } + + // And finally rename persistent to default. + QM_TRY(MOZ_TO_RESULT(aPersistentStorageDir->RenameTo( + nullptr, nsLiteralString(DEFAULT_DIRECTORY_NAME)))); + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::UpgradeFromPersistentStorageDirectory, innerFunc); +} + +template <typename Helper> +nsresult QuotaManager::UpgradeStorage(const int32_t aOldVersion, + const int32_t aNewVersion, + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aNewVersion > aOldVersion); + MOZ_ASSERT(aNewVersion <= kStorageVersion); + MOZ_ASSERT(aConnection); + + for (const PersistenceType persistenceType : kAllPersistenceTypes) { + QM_TRY_UNWRAP(auto directory, + QM_NewLocalFile(GetStoragePath(persistenceType))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (!exists) { + continue; + } + + RefPtr<UpgradeStorageHelperBase> helper = new Helper(directory); + + QM_TRY(MOZ_TO_RESULT(helper->Init())); + + QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository())); + } + +#ifdef DEBUG + { + QM_TRY_INSPECT(const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == aOldVersion); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(aNewVersion))); + + return NS_OK; +} + +nsresult QuotaManager::UpgradeStorageFrom0_0To1_0( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom0_0To1_0Helper>( + 0, MakeStorageVersion(1, 0), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom0_0To1_0, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom1_0To2_0( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The upgrade consists of a number of logically distinct bugs that + // intentionally got fixed at the same time to trigger just one major + // version bump. + // + // + // Morgue directory cleanup + // [Feature/Bug]: + // The original bug that added "on demand" morgue cleanup is 1165119. + // + // [Mutations]: + // Morgue directories are removed from all origin directories during the + // upgrade process. Origin initialization and usage calculation doesn't try + // to remove morgue directories anymore. + // + // [Downgrade-incompatible changes]: + // Morgue directories can reappear if user runs an already upgraded profile + // in an older version of Firefox. Morgue directories then prevent current + // Firefox from initializing and using the storage. + // + // + // App data removal + // [Feature/Bug]: + // The bug that removes isApp flags is 1311057. + // + // [Mutations]: + // Origin directories with appIds are removed during the upgrade process. + // + // [Downgrade-incompatible changes]: + // Origin directories with appIds can reappear if user runs an already + // upgraded profile in an older version of Firefox. Origin directories with + // appIds don't prevent current Firefox from initializing and using the + // storage, but they wouldn't ever be removed again, potentially causing + // problems once appId is removed from origin attributes. + // + // + // Strip obsolete origin attributes + // [Feature/Bug]: + // The bug that strips obsolete origin attributes is 1314361. + // + // [Mutations]: + // Origin directories with obsolete origin attributes are renamed and their + // metadata files are updated during the upgrade process. + // + // [Downgrade-incompatible changes]: + // Origin directories with obsolete origin attributes can reappear if user + // runs an already upgraded profile in an older version of Firefox. Origin + // directories with obsolete origin attributes don't prevent current Firefox + // from initializing and using the storage, but they wouldn't ever be upgraded + // again, potentially causing problems in future. + // + // + // File manager directory renaming (client specific) + // [Feature/Bug]: + // The original bug that added "on demand" file manager directory renaming is + // 1056939. + // + // [Mutations]: + // All file manager directories are renamed to contain the ".files" suffix. + // + // [Downgrade-incompatible changes]: + // File manager directories with the ".files" suffix prevent older versions of + // Firefox from initializing and using the storage. + // File manager directories without the ".files" suffix can appear if user + // runs an already upgraded profile in an older version of Firefox. File + // manager directories without the ".files" suffix then prevent current + // Firefox from initializing and using the storage. + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom1_0To2_0Helper>( + MakeStorageVersion(1, 0), MakeStorageVersion(2, 0), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom1_0To2_0, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom2_0To2_1( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The upgrade is mainly to create a directory padding file in DOM Cache + // directory to record the overall padding size of an origin. + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_0To2_1Helper>( + MakeStorageVersion(2, 0), MakeStorageVersion(2, 1), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom2_0To2_1, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom2_1To2_2( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + // The upgrade is mainly to clean obsolete origins in the repositoies, remove + // asmjs client, and ".tmp" file in the idb folers. + + const auto innerFunc = [this, &aConnection](const auto&) -> nsresult { + QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_1To2_2Helper>( + MakeStorageVersion(2, 1), MakeStorageVersion(2, 2), aConnection))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom2_1To2_2, + innerFunc); +} + +nsresult QuotaManager::UpgradeStorageFrom2_2To2_3( + mozIStorageConnection* aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(aConnection); + + const auto innerFunc = [&aConnection](const auto&) -> nsresult { + // Table `database` + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE database" + "( cache_version INTEGER NOT NULL DEFAULT 0" + ");")))); + + QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO database (cache_version) " + "VALUES (0)")))); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + MOZ_ASSERT(storageVersion == MakeStorageVersion(2, 2)); + } +#endif + + QM_TRY( + MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeStorageVersion(2, 3)))); + + return NS_OK; + }; + + return ExecuteInitialization(Initialization::UpgradeStorageFrom2_2To2_3, + innerFunc); +} + +nsresult QuotaManager::MaybeRemoveLocalStorageDataAndArchive( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(!CachedNextGenLocalStorageEnabled()); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + if (!exists) { + // If the ls archive doesn't exist then ls directories can't exist either. + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories())); + + InvalidateQuotaCache(); + + // Finally remove the ls archive, so we don't have to check all origin + // directories next time this method is called. + QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false))); + + return NS_OK; +} + +nsresult QuotaManager::MaybeRemoveLocalStorageDirectories() { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& defaultStorageDir, + QM_NewLocalFile(*mDefaultStoragePath)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists)); + + if (!exists) { + return NS_OK; + } + + QM_TRY(CollectEachFile( + *defaultStorageDir, + [](const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> { +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(originDir, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + QM_TRY_INSPECT( + const auto& lsDir, + CloneFileAndAppend(*originDir, NS_LITERAL_STRING_FROM_CSTRING( + LS_DIRECTORY_NAME))); + + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, Exists)); + + if (!exists) { + return Ok{}; + } + } + + { + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, IsDirectory)); + + if (!isDirectory) { + QM_WARNING("ls entry is not a directory!"); + + return Ok{}; + } + } + + nsString path; + QM_TRY(MOZ_TO_RESULT(lsDir->GetPath(path))); + + QM_WARNING("Deleting %s directory!", + NS_ConvertUTF16toUTF8(path).get()); + + QM_TRY(MOZ_TO_RESULT(lsDir->Remove(/* aRecursive */ true))); + + break; + } + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, originDir, GetLeafName)); + + // Unknown files during upgrade are allowed. Just warn if we find + // them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + return Ok{}; + })); + + return NS_OK; +} + +Result<Ok, nsresult> QuotaManager::CopyLocalStorageArchiveFromWebAppsStore( + nsIFile& aLsArchiveFile) const { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + MOZ_ASSERT(!exists); + } +#endif + + // Get the storage service first, we will need it at multiple places. + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + // Get the web apps store file. + QM_TRY_INSPECT(const auto& webAppsStoreFile, QM_NewLocalFile(mBasePath)); + + QM_TRY(MOZ_TO_RESULT( + webAppsStoreFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME)))); + + // Now check if the web apps store is useable. + QM_TRY_INSPECT(const auto& connection, + CreateWebAppsStoreConnection(*webAppsStoreFile, *ss)); + + if (connection) { + // Find out the journal mode. + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement( + *connection, "PRAGMA journal_mode;"_ns)); + + QM_TRY_INSPECT(const auto& journalMode, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt, + GetUTF8String, 0)); + + QM_TRY(MOZ_TO_RESULT(stmt->Finalize())); + + if (journalMode.EqualsLiteral("wal")) { + // We don't copy the WAL file, so make sure the old database is fully + // checkpointed. + QM_TRY(MOZ_TO_RESULT( + connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(TRUNCATE);"_ns))); + } + + // Explicitely close the connection before the old database is copied. + QM_TRY(MOZ_TO_RESULT(connection->Close())); + + // Copy the old database. The database is copied from + // <profile>/webappsstore.sqlite to + // <profile>/storage/ls-archive-tmp.sqlite + // We use a "-tmp" postfix since we are not done yet. + QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath)); + + QM_TRY(MOZ_TO_RESULT(webAppsStoreFile->CopyTo( + storageDir, nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& lsArchiveTmpFile, + GetLocalStorageArchiveTmpFile(*mStoragePath)); + + if (journalMode.EqualsLiteral("wal")) { + QM_TRY_INSPECT( + const auto& lsArchiveTmpConnection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + lsArchiveTmpFile, mozIStorageService::CONNECTION_DEFAULT)); + + // The archive will only be used for lazy data migration. There won't be + // any concurrent readers and writers that could benefit from Write-Ahead + // Logging. So switch to a standard rollback journal. The standard + // rollback journal also provides atomicity across multiple attached + // databases which is import for the lazy data migration to work safely. + QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->ExecuteSimpleSQL( + "PRAGMA journal_mode = DELETE;"_ns))); + + // Close the connection explicitly. We are going to rename the file below. + QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->Close())); + } + + // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite + QM_TRY(MOZ_TO_RESULT(lsArchiveTmpFile->MoveTo( + nullptr, nsLiteralString(LS_ARCHIVE_FILE_NAME)))); + + return Ok{}; + } + + // If webappsstore database is not useable, just create an empty archive. + // XXX The code below should be removed and the caller should call us only + // when webappstore.sqlite exists. CreateWebAppsStoreConnection should be + // reworked to propagate database corruption instead of returning null + // connection. + // So, if there's no webappsstore.sqlite + // MaybeCreateOrUpgradeLocalStorageArchive will call + // CreateEmptyLocalStorageArchive instead of + // CopyLocalStorageArchiveFromWebAppsStore. + // If there's any corruption detected during + // MaybeCreateOrUpgradeLocalStorageArchive (including nested calls like + // CopyLocalStorageArchiveFromWebAppsStore and CreateWebAppsStoreConnection) + // EnsureStorageIsInitialized will fallback to + // CreateEmptyLocalStorageArchive. + + // Ensure the storage directory actually exists. + QM_TRY_INSPECT(const auto& storageDirectory, QM_NewLocalFile(*mStoragePath)); + + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDirectory)); + + Unused << created; + + QM_TRY_UNWRAP(auto lsArchiveConnection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT)); + + QM_TRY(MOZ_TO_RESULT( + StorageDBUpdater::CreateCurrentSchema(lsArchiveConnection))); + + return Ok{}; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::CreateLocalStorageArchiveConnection( + nsIFile& aLsArchiveFile) const { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, IsDirectory)); + + // A directory with the name of the archive file is treated as corruption + // (similarly as wrong content of the file). + QM_TRY(OkIf(!isDirectory), Err(NS_ERROR_FILE_CORRUPTED)); + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + // This may return NS_ERROR_FILE_CORRUPTED too. + QM_TRY_UNWRAP(auto connection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT)); + + // The legacy LS implementation removes the database and creates an empty one + // when the schema can't be updated. The same effect can be achieved here by + // mapping all errors to NS_ERROR_FILE_CORRUPTED. One such case is tested by + // sub test case 3 of dom/localstorage/test/unit/test_archive.js + QM_TRY( + MOZ_TO_RESULT(StorageDBUpdater::Update(connection)) + .mapErr([](const nsresult rv) { return NS_ERROR_FILE_CORRUPTED; })); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::RecopyLocalStorageArchiveFromWebAppsStore( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories())); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + MOZ_ASSERT(exists); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false))); + + QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + + QM_TRY_UNWRAP(auto connection, + CreateLocalStorageArchiveConnection(aLsArchiveFile)); + + QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection))); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::DowngradeLocalStorageArchive(nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY_UNWRAP(auto connection, + RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + + QM_TRY(MOZ_TO_RESULT( + SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion))); + + return connection; +} + +Result<nsCOMPtr<mozIStorageConnection>, nsresult> +QuotaManager::UpgradeLocalStorageArchiveFromLessThan4To4( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + QM_TRY_UNWRAP(auto connection, + RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + + QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion(connection, 4))); + + return connection; +} + +/* +nsresult QuotaManager::UpgradeLocalStorageArchiveFrom4To5( + nsCOMPtr<mozIStorageConnection>& aConnection) { + AssertIsOnIOThread(); + MOZ_ASSERT(CachedNextGenLocalStorageEnabled()); + + nsresult rv = SaveLocalStorageArchiveVersion(aConnection, 5); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} +*/ + +#ifdef DEBUG + +void QuotaManager::AssertStorageIsInitialized() const { + AssertIsOnIOThread(); + MOZ_ASSERT(IsStorageInitialized()); +} + +#endif // DEBUG + +nsresult QuotaManager::MaybeUpgradeToDefaultStorageDirectory( + nsIFile& aStorageFile) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& storageFileExists, + MOZ_TO_RESULT_INVOKE_MEMBER(aStorageFile, Exists)); + + if (!storageFileExists) { + QM_TRY_INSPECT(const auto& indexedDBDir, QM_NewLocalFile(*mIndexedDBPath)); + + QM_TRY_INSPECT(const auto& indexedDBDirExists, + MOZ_TO_RESULT_INVOKE_MEMBER(indexedDBDir, Exists)); + + if (indexedDBDirExists) { + QM_TRY(MOZ_TO_RESULT( + UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory( + indexedDBDir))); + } + + QM_TRY_INSPECT(const auto& persistentStorageDir, + QM_NewLocalFile(*mStoragePath)); + + QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append( + nsLiteralString(PERSISTENT_DIRECTORY_NAME)))); + + QM_TRY_INSPECT(const auto& persistentStorageDirExists, + MOZ_TO_RESULT_INVOKE_MEMBER(persistentStorageDir, Exists)); + + if (persistentStorageDirExists) { + QM_TRY(MOZ_TO_RESULT( + UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory( + persistentStorageDir))); + } + } + + return NS_OK; +} + +nsresult QuotaManager::MaybeCreateOrUpgradeStorage( + mozIStorageConnection& aConnection) { + AssertIsOnIOThread(); + + QM_TRY_UNWRAP(auto storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion)); + + // Hacky downgrade logic! + // If we see major.minor of 3.0, downgrade it to be 2.1. + if (storageVersion == kHackyPreDowngradeStorageVersion) { + storageVersion = kHackyPostDowngradeStorageVersion; + QM_TRY(MOZ_TO_RESULT(aConnection.SetSchemaVersion(storageVersion)), + QM_PROPAGATE, + [](const auto&) { MOZ_ASSERT(false, "Downgrade didn't take."); }); + } + + QM_TRY(OkIf(GetMajorStorageVersion(storageVersion) <= kMajorStorageVersion), + NS_ERROR_FAILURE, [](const auto&) { + NS_WARNING("Unable to initialize storage, version is too high!"); + }); + + if (storageVersion < kStorageVersion) { + const bool newDatabase = !storageVersion; + + QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath)); + + QM_TRY_INSPECT(const auto& storageDirExists, + MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, Exists)); + + const bool newDirectory = !storageDirExists; + + if (newDatabase) { + // Set the page size first. + if (kSQLitePageSizeOverride) { + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride)))); + } + } + + mozStorageTransaction transaction( + &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(transaction.Start())); + + // An upgrade method can upgrade the database, the storage or both. + // The upgrade loop below can only be avoided when there's no database and + // no storage yet (e.g. new profile). + if (newDatabase && newDirectory) { + QM_TRY(MOZ_TO_RESULT(CreateTables(&aConnection))); + +#ifdef DEBUG + { + QM_TRY_INSPECT( + const int32_t& storageVersion, + MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(storageVersion == kStorageVersion); + } +#endif + + QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO database (cache_version) " + "VALUES (0)")))); + } else { + // This logic needs to change next time we change the storage! + static_assert(kStorageVersion == int32_t((2 << 16) + 3), + "Upgrade function needed due to storage version increase."); + + while (storageVersion != kStorageVersion) { + if (storageVersion == 0) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom0_0To1_0(&aConnection))); + } else if (storageVersion == MakeStorageVersion(1, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom1_0To2_0(&aConnection))); + } else if (storageVersion == MakeStorageVersion(2, 0)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_0To2_1(&aConnection))); + } else if (storageVersion == MakeStorageVersion(2, 1)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_1To2_2(&aConnection))); + } else if (storageVersion == MakeStorageVersion(2, 2)) { + QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_2To2_3(&aConnection))); + } else { + QM_FAIL(NS_ERROR_FAILURE, []() { + NS_WARNING( + "Unable to initialize storage, no upgrade path is " + "available!"); + }); + } + + QM_TRY_UNWRAP(storageVersion, MOZ_TO_RESULT_INVOKE_MEMBER( + aConnection, GetSchemaVersion)); + } + + MOZ_ASSERT(storageVersion == kStorageVersion); + } + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + } + + return NS_OK; +} + +OkOrErr QuotaManager::MaybeRemoveLocalStorageArchiveTmpFile() { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& lsArchiveTmpFile, + QM_TO_RESULT_TRANSFORM(GetLocalStorageArchiveTmpFile(*mStoragePath))); + + QM_TRY_INSPECT(const bool& exists, + QM_TO_RESULT_INVOKE_MEMBER(lsArchiveTmpFile, Exists)); + + if (exists) { + QM_TRY(QM_TO_RESULT(lsArchiveTmpFile->Remove(false))); + } + + return Ok{}; +} + +Result<Ok, nsresult> QuotaManager::MaybeCreateOrUpgradeLocalStorageArchive( + nsIFile& aLsArchiveFile) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const bool& lsArchiveFileExisted, + ([this, &aLsArchiveFile]() -> Result<bool, nsresult> { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + if (!exists) { + QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile)); + } + + return exists; + }())); + + QM_TRY_UNWRAP(auto connection, + CreateLocalStorageArchiveConnection(aLsArchiveFile)); + + QM_TRY_INSPECT(const auto& initialized, + IsLocalStorageArchiveInitialized(*connection)); + + if (!initialized) { + QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection))); + } + + QM_TRY_UNWRAP(int32_t version, LoadLocalStorageArchiveVersion(*connection)); + + if (version > kLocalStorageArchiveVersion) { + // Close local storage archive connection. We are going to remove underlying + // file. + QM_TRY(MOZ_TO_RESULT(connection->Close())); + + // This will wipe the archive and any migrated data and recopy the archive + // from webappsstore.sqlite. + QM_TRY_UNWRAP(connection, DowngradeLocalStorageArchive(aLsArchiveFile)); + + QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection)); + + MOZ_ASSERT(version == kLocalStorageArchiveVersion); + } else if (version != kLocalStorageArchiveVersion) { + // The version can be zero either when the archive didn't exist or it did + // exist, but the archive was created without any version information. + // We don't need to do any upgrades only if it didn't exist because existing + // archives without version information must be recopied to really fix bug + // 1542104. See also bug 1546305 which introduced archive versions. + if (!lsArchiveFileExisted) { + MOZ_ASSERT(version == 0); + + QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion( + connection, kLocalStorageArchiveVersion))); + } else { + static_assert(kLocalStorageArchiveVersion == 4, + "Upgrade function needed due to LocalStorage archive " + "version increase."); + + while (version != kLocalStorageArchiveVersion) { + if (version < 4) { + // Close local storage archive connection. We are going to remove + // underlying file. + QM_TRY(MOZ_TO_RESULT(connection->Close())); + + // This won't do an "upgrade" in a normal sense. It will wipe the + // archive and any migrated data and recopy the archive from + // webappsstore.sqlite + QM_TRY_UNWRAP(connection, UpgradeLocalStorageArchiveFromLessThan4To4( + aLsArchiveFile)); + } /* else if (version == 4) { + QM_TRY(MOZ_TO_RESULT(UpgradeLocalStorageArchiveFrom4To5(connection))); + } */ + else { + QM_FAIL(Err(NS_ERROR_FAILURE), []() { + QM_WARNING( + "Unable to initialize LocalStorage archive, no upgrade path " + "is available!"); + }); + } + + QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection)); + } + + MOZ_ASSERT(version == kLocalStorageArchiveVersion); + } + } + + // At this point, we have finished initializing the local storage archive, and + // can continue storage initialization. We don't know though if the actual + // data in the archive file is readable. We can't do a PRAGMA integrity_check + // here though, because that would be too heavyweight. + + return Ok{}; +} + +Result<Ok, nsresult> QuotaManager::CreateEmptyLocalStorageArchive( + nsIFile& aLsArchiveFile) const { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists)); + + // If it exists, remove it. It might be a directory, so remove it recursively. + if (exists) { + QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(true))); + + // XXX If we crash right here, the next session will copy the archive from + // webappsstore.sqlite again! + // XXX Create a marker file before removing the archive which can be + // used in MaybeCreateOrUpgradeLocalStorageArchive to create an empty + // archive instead of recopying it from webapppstore.sqlite (in other + // words, finishing what was started here). + } + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP(const auto connection, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT)); + + QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection))); + + QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection))); + + QM_TRY(MOZ_TO_RESULT( + SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion))); + + return Ok{}; +} + +nsresult QuotaManager::EnsureStorageIsInitialized() { + DiagnosticAssertIsOnIOThread(); + + const auto innerFunc = + [&](const auto& firstInitializationAttempt) -> nsresult { + if (mStorageConnection) { + MOZ_ASSERT(firstInitializationAttempt.Recorded()); + return NS_OK; + } + + QM_TRY_INSPECT(const auto& storageFile, QM_NewLocalFile(mBasePath)); + QM_TRY(MOZ_TO_RESULT(storageFile->Append(mStorageName + kSQLiteSuffix))); + + QM_TRY(MOZ_TO_RESULT(MaybeUpgradeToDefaultStorageDirectory(*storageFile))); + + QM_TRY_INSPECT(const auto& ss, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + QM_TRY_UNWRAP( + auto connection, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase, + storageFile, mozIStorageService::CONNECTION_DEFAULT), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>)); + + if (!connection) { + // Nuke the database file. + QM_TRY(MOZ_TO_RESULT(storageFile->Remove(false))); + + QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, ss, + OpenUnsharedDatabase, storageFile, + mozIStorageService::CONNECTION_DEFAULT)); + } + + // We want extra durability for this important file. + QM_TRY(MOZ_TO_RESULT( + connection->ExecuteSimpleSQL("PRAGMA synchronous = EXTRA;"_ns))); + + // Check to make sure that the storage version is correct. + QM_TRY(MOZ_TO_RESULT(MaybeCreateOrUpgradeStorage(*connection))); + + QM_TRY(MaybeRemoveLocalStorageArchiveTmpFile()); + + QM_TRY_INSPECT(const auto& lsArchiveFile, + GetLocalStorageArchiveFile(*mStoragePath)); + + if (CachedNextGenLocalStorageEnabled()) { + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MaybeCreateOrUpgradeLocalStorageArchive(*lsArchiveFile), + // Predicate. + IsDatabaseCorruptionError, + // Fallback. + ([&](const nsresult rv) -> Result<Ok, nsresult> { + QM_TRY_RETURN(CreateEmptyLocalStorageArchive(*lsArchiveFile)); + }))); + } else { + QM_TRY( + MOZ_TO_RESULT(MaybeRemoveLocalStorageDataAndArchive(*lsArchiveFile))); + } + + QM_TRY_UNWRAP(mCacheUsable, MaybeCreateOrUpgradeCache(*connection)); + + if (mCacheUsable && gInvalidateQuotaCache) { + QM_TRY(InvalidateCache(*connection)); + + gInvalidateQuotaCache = false; + } + + mStorageConnection = std::move(connection); + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::Storage, + "dom::quota::FirstInitializationAttempt::Storage"_ns, innerFunc); +} + +RefPtr<ClientDirectoryLock> QuotaManager::CreateDirectoryLock( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, + Client::Type aClientType, bool aExclusive) { + AssertIsOnOwningThread(); + + return DirectoryLockImpl::Create(WrapNotNullUnchecked(this), aPersistenceType, + aOriginMetadata, aClientType, aExclusive); +} + +RefPtr<UniversalDirectoryLock> QuotaManager::CreateDirectoryLockInternal( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + bool aExclusive) { + AssertIsOnOwningThread(); + + return DirectoryLockImpl::CreateInternal(WrapNotNullUnchecked(this), + aPersistenceType, aOriginScope, + aClientType, aExclusive); +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsurePersistentOriginIsInitialized( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == PERSISTENCE_TYPE_PERSISTENT); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + const auto innerFunc = [&aOriginMetadata, + this](const auto& firstInitializationAttempt) + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aOriginMetadata)); + + if (mInitializedOrigins.Contains(aOriginMetadata.mOrigin)) { + MOZ_ASSERT(firstInitializationAttempt.Recorded()); + return std::pair(std::move(directory), false); + } + + QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory)); + + QM_TRY_INSPECT( + const int64_t& timestamp, + ([this, created, &directory, + &aOriginMetadata]() -> Result<int64_t, nsresult> { + if (created) { + const int64_t timestamp = PR_Now(); + + // Only creating .metadata-v2 to reduce IO. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ true, + aOriginMetadata))); + + return timestamp; + } + + // Get the metadata. We only use the timestamp. + QM_TRY_INSPECT(const auto& metadata, + LoadFullOriginMetadataWithRestore(directory)); + + MOZ_ASSERT(metadata.mLastAccessTime <= PR_Now()); + + return metadata.mLastAccessTime; + }())); + + QM_TRY(MOZ_TO_RESULT(InitializeOrigin(PERSISTENCE_TYPE_PERSISTENT, + aOriginMetadata, timestamp, + /* aPersisted */ true, directory))); + + mInitializedOrigins.AppendElement(aOriginMetadata.mOrigin); + + return std::pair(std::move(directory), created); + }; + + return ExecuteOriginInitialization( + aOriginMetadata.mOrigin, OriginInitialization::PersistentOrigin, + "dom::quota::FirstOriginInitializationAttempt::PersistentOrigin"_ns, + innerFunc); +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsureTemporaryOriginIsInitialized( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized); + + const auto innerFunc = [&aOriginMetadata, this](const auto&) + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + // Get directory for this origin and persistence type. + QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aOriginMetadata)); + + QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory)); + + if (created) { + const int64_t timestamp = + NoteOriginDirectoryCreated(aOriginMetadata, /* aPersisted */ false); + + // Only creating .metadata-v2 to reduce IO. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ false, + aOriginMetadata))); + } + + // TODO: If the metadata file exists and we didn't call + // LoadFullOriginMetadataWithRestore for it (because the quota info + // was loaded from the cache), then the group in the metadata file + // may be wrong, so it should be checked and eventually updated. + // It's not a big deal that we are not doing it here, because the + // origin will be marked as "accessed", so + // LoadFullOriginMetadataWithRestore will be called for the metadata + // file in next session in LoadQuotaFromCache. + + return std::pair(std::move(directory), created); + }; + + return ExecuteOriginInitialization( + aOriginMetadata.mOrigin, OriginInitialization::TemporaryOrigin, + "dom::quota::FirstOriginInitializationAttempt::TemporaryOrigin"_ns, + innerFunc); +} + +nsresult QuotaManager::EnsureTemporaryStorageIsInitialized() { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + const auto innerFunc = + [&](const auto& firstInitializationAttempt) -> nsresult { + if (mTemporaryStorageInitialized) { + MOZ_ASSERT(firstInitializationAttempt.Recorded()); + return NS_OK; + } + + QM_TRY_INSPECT( + const auto& storageDir, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIFile>, + MOZ_SELECT_OVERLOAD(do_CreateInstance), + NS_LOCAL_FILE_CONTRACTID)); + + QM_TRY(MOZ_TO_RESULT(storageDir->InitWithPath(GetStoragePath()))); + + // The storage directory must exist before calling GetTemporaryStorageLimit. + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDir)); + + Unused << created; + + QM_TRY_UNWRAP(mTemporaryStorageLimit, + GetTemporaryStorageLimit(*storageDir)); + + QM_TRY(MOZ_TO_RESULT(LoadQuota())); + + mTemporaryStorageInitialized = true; + + CleanupTemporaryStorage(); + + if (mCacheUsable) { + QM_TRY(InvalidateCache(*mStorageConnection)); + } + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::TemporaryStorage, + "dom::quota::FirstInitializationAttempt::TemporaryStorage"_ns, innerFunc); +} + +RefPtr<BoolPromise> QuotaManager::ClearPrivateRepository() { + auto clearPrivateRepositoryOp = MakeRefPtr<ClearPrivateRepositoryOp>(); + + RegisterNormalOriginOp(*clearPrivateRepositoryOp); + + clearPrivateRepositoryOp->RunImmediately(); + + return clearPrivateRepositoryOp->OnResults(); +} + +RefPtr<BoolPromise> QuotaManager::ShutdownStorage() { + if (!mShuttingDownStorage) { + mShuttingDownStorage = true; + + auto shutdownStorageOp = MakeRefPtr<ShutdownStorageOp>(); + + RegisterNormalOriginOp(*shutdownStorageOp); + + shutdownStorageOp->RunImmediately(); + + shutdownStorageOp->OnResults()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<QuotaManager>(this)](bool aResolveValue) { + self->mShuttingDownStorage = false; + + self->mShutdownStoragePromiseHolder.ResolveIfExists(aResolveValue, + __func__); + }, + [self = RefPtr<QuotaManager>(this)](nsresult aRejectValue) { + self->mShuttingDownStorage = false; + + self->mShutdownStoragePromiseHolder.RejectIfExists(aRejectValue, + __func__); + }); + } + + return mShutdownStoragePromiseHolder.Ensure(__func__); +} + +void QuotaManager::ShutdownStorageInternal() { + AssertIsOnIOThread(); + + if (mStorageConnection) { + mInitializationInfo.ResetOriginInitializationInfos(); + mInitializedOrigins.Clear(); + + if (mTemporaryStorageInitialized) { + if (mCacheUsable) { + UnloadQuota(); + } else { + RemoveQuota(); + } + + mTemporaryStorageInitialized = false; + } + + ReleaseIOThreadObjects(); + + mStorageConnection = nullptr; + mCacheUsable = false; + } + + mInitializationInfo.ResetFirstInitializationAttempts(); +} + +Result<bool, nsresult> QuotaManager::EnsureOriginDirectory( + nsIFile& aDirectory) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists)); + + if (!exists) { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aDirectory, GetLeafName) + .map([](const auto& leafName) { + return NS_ConvertUTF16toUTF8(leafName); + })); + + QM_TRY(OkIf(IsSanitizedOriginValid(leafName)), Err(NS_ERROR_FAILURE), + [](const auto&) { + QM_WARNING( + "Preventing creation of a new origin directory which is not " + "supported by our origin parser or is obsolete!"); + }); + } + + QM_TRY_RETURN(EnsureDirectory(aDirectory)); +} + +nsresult QuotaManager::AboutToClearOrigins( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, + const Nullable<Client::Type>& aClientType) { + AssertIsOnIOThread(); + + if (aClientType.IsNull()) { + for (Client::Type type : AllClientTypes()) { + QM_TRY(MOZ_TO_RESULT((*mClients)[type]->AboutToClearOrigins( + aPersistenceType, aOriginScope))); + } + } else { + QM_TRY(MOZ_TO_RESULT((*mClients)[aClientType.Value()]->AboutToClearOrigins( + aPersistenceType, aOriginScope))); + } + + return NS_OK; +} + +void QuotaManager::OriginClearCompleted( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const Nullable<Client::Type>& aClientType) { + AssertIsOnIOThread(); + + if (aClientType.IsNull()) { + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + mInitializedOrigins.RemoveElement(aOrigin); + } + + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->OnOriginClearCompleted(aPersistenceType, aOrigin); + } + } else { + (*mClients)[aClientType.Value()]->OnOriginClearCompleted(aPersistenceType, + aOrigin); + } +} + +void QuotaManager::RepositoryClearCompleted(PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + mInitializedOrigins.Clear(); + } + + for (Client::Type type : AllClientTypes()) { + (*mClients)[type]->OnRepositoryClearCompleted(aPersistenceType); + } +} + +Client* QuotaManager::GetClient(Client::Type aClientType) { + MOZ_ASSERT(aClientType >= Client::IDB); + MOZ_ASSERT(aClientType < Client::TypeMax()); + + return (*mClients)[aClientType]; +} + +const AutoTArray<Client::Type, Client::TYPE_MAX>& +QuotaManager::AllClientTypes() { + if (CachedNextGenLocalStorageEnabled()) { + return *mAllClientTypes; + } + return *mAllClientTypesExceptLS; +} + +uint64_t QuotaManager::GetGroupLimit() const { + // To avoid one group evicting all the rest, limit the amount any one group + // can use to 20% resp. a fifth. To prevent individual sites from using + // exorbitant amounts of storage where there is a lot of free space, cap the + // group limit to 10GB. + const auto x = std::min<uint64_t>(mTemporaryStorageLimit / 5, 10 GB); + + // In low-storage situations, make an exception (while not exceeding the total + // storage limit). + return std::min<uint64_t>(mTemporaryStorageLimit, + std::max<uint64_t>(x, 10 MB)); +} + +std::pair<uint64_t, uint64_t> QuotaManager::GetUsageAndLimitForEstimate( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + + uint64_t totalGroupUsage = 0; + + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (groupInfo) { + if (type == PERSISTENCE_TYPE_DEFAULT) { + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + + if (originInfo && originInfo->LockedPersisted()) { + return std::pair(mTemporaryStorageUsage, mTemporaryStorageLimit); + } + } + + AssertNoOverflow(totalGroupUsage, groupInfo->mUsage); + totalGroupUsage += groupInfo->mUsage; + } + } + } + } + + return std::pair(totalGroupUsage, GetGroupLimit()); +} + +uint64_t QuotaManager::GetOriginUsage( + const PrincipalMetadata& aPrincipalMetadata) { + AssertIsOnIOThread(); + + uint64_t usage = 0; + + { + MutexAutoLock lock(mQuotaMutex); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aPrincipalMetadata.mGroup, &pair)) { + for (const PersistenceType type : kBestEffortPersistenceTypes) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type); + if (groupInfo) { + RefPtr<OriginInfo> originInfo = + groupInfo->LockedGetOriginInfo(aPrincipalMetadata.mOrigin); + if (originInfo) { + AssertNoOverflow(usage, originInfo->LockedUsage()); + usage += originInfo->LockedUsage(); + } + } + } + } + } + + return usage; +} + +Maybe<FullOriginMetadata> QuotaManager::GetFullOriginMetadata( + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(aOriginMetadata.mPersistenceType, aOriginMetadata); + if (originInfo) { + return Some(originInfo->LockedFlattenToFullOriginMetadata()); + } + + return Nothing(); +} + +void QuotaManager::NotifyStoragePressure(uint64_t aUsage) { + mQuotaMutex.AssertNotCurrentThreadOwns(); + + RefPtr<StoragePressureRunnable> storagePressureRunnable = + new StoragePressureRunnable(aUsage); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(storagePressureRunnable)); +} + +// static +void QuotaManager::GetStorageId(PersistenceType aPersistenceType, + const nsACString& aOrigin, + Client::Type aClientType, + nsACString& aDatabaseId) { + nsAutoCString str; + str.AppendInt(aPersistenceType); + str.Append('*'); + str.Append(aOrigin); + str.Append('*'); + str.AppendInt(aClientType); + + aDatabaseId = str; +} + +// static +bool QuotaManager::IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo) { + switch (aPrincipalInfo.type()) { + // A system principal is acceptable. + case PrincipalInfo::TSystemPrincipalInfo: { + return true; + } + + // Validate content principals to ensure that the spec, originNoSuffix and + // baseDomain are sane. + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + // Verify the principal spec parses. + nsCOMPtr<nsIURI> uri; + QM_TRY(MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), info.spec())), false); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, info.attrs()); + QM_TRY(MOZ_TO_RESULT(principal), false); + + // Verify the principal originNoSuffix matches spec. + QM_TRY_INSPECT(const auto& originNoSuffix, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal, + GetOriginNoSuffix), + false); + + if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) { + QM_WARNING("originNoSuffix (%s) doesn't match passed one (%s)!", + originNoSuffix.get(), info.originNoSuffix().get()); + return false; + } + + if (NS_WARN_IF(info.originNoSuffix().EqualsLiteral(kChromeOrigin))) { + return false; + } + + if (NS_WARN_IF(info.originNoSuffix().FindChar('^', 0) != -1)) { + QM_WARNING("originNoSuffix (%s) contains the '^' character!", + info.originNoSuffix().get()); + return false; + } + + // Verify the principal baseDomain exists. + if (NS_WARN_IF(info.baseDomain().IsVoid())) { + return false; + } + + // Verify the principal baseDomain matches spec. + QM_TRY_INSPECT(const auto& baseDomain, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, principal, + GetBaseDomain), + false); + + if (NS_WARN_IF(baseDomain != info.baseDomain())) { + QM_WARNING("baseDomain (%s) doesn't match passed one (%s)!", + baseDomain.get(), info.baseDomain().get()); + return false; + } + + return true; + } + + default: { + break; + } + } + + // Null and expanded principals are not acceptable. + return false; +} + +Result<PrincipalMetadata, nsresult> +QuotaManager::GetInfoFromValidatedPrincipalInfo( + const PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo)); + + switch (aPrincipalInfo.type()) { + case PrincipalInfo::TSystemPrincipalInfo: { + return GetInfoForChrome(); + } + + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + nsCString suffix; + info.attrs().CreateSuffix(suffix); + + nsCString origin = info.originNoSuffix() + suffix; + + if (StringBeginsWith(origin, kUUIDOriginScheme)) { + QM_TRY_INSPECT(const auto& originalOrigin, + GetOriginFromStorageOrigin(origin)); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(originalOrigin); + QM_TRY(MOZ_TO_RESULT(principal)); + + PrincipalInfo principalInfo; + QM_TRY( + MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo))); + + return GetInfoFromValidatedPrincipalInfo(principalInfo); + } + + PrincipalMetadata principalMetadata; + + principalMetadata.mSuffix = suffix; + + principalMetadata.mGroup = info.baseDomain() + suffix; + + principalMetadata.mOrigin = origin; + + if (info.attrs().mPrivateBrowsingId != 0) { + QM_TRY_UNWRAP(principalMetadata.mStorageOrigin, + EnsureStorageOriginFromOrigin(origin)); + } else { + principalMetadata.mStorageOrigin = origin; + } + + principalMetadata.mIsPrivate = info.attrs().mPrivateBrowsingId != 0; + + return principalMetadata; + } + + default: { + MOZ_ASSERT_UNREACHABLE("Should never get here!"); + return Err(NS_ERROR_UNEXPECTED); + } + } +} + +// static +nsAutoCString QuotaManager::GetOriginFromValidatedPrincipalInfo( + const PrincipalInfo& aPrincipalInfo) { + MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo)); + + switch (aPrincipalInfo.type()) { + case PrincipalInfo::TSystemPrincipalInfo: { + return nsAutoCString{GetOriginForChrome()}; + } + + case PrincipalInfo::TContentPrincipalInfo: { + const ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + + nsAutoCString suffix; + + info.attrs().CreateSuffix(suffix); + + return info.originNoSuffix() + suffix; + } + + default: { + MOZ_CRASH("Should never get here!"); + } + } +} + +// static +Result<PrincipalMetadata, nsresult> QuotaManager::GetInfoFromPrincipal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + if (aPrincipal->IsSystemPrincipal()) { + return GetInfoForChrome(); + } + + if (aPrincipal->GetIsNullPrincipal()) { + NS_WARNING("IndexedDB not supported from this principal!"); + return Err(NS_ERROR_FAILURE); + } + + PrincipalMetadata principalMetadata; + + QM_TRY(MOZ_TO_RESULT(aPrincipal->GetOrigin(principalMetadata.mOrigin))); + + if (principalMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) { + NS_WARNING("Non-chrome principal can't use chrome origin!"); + return Err(NS_ERROR_FAILURE); + } + + aPrincipal->OriginAttributesRef().CreateSuffix(principalMetadata.mSuffix); + + nsAutoCString baseDomain; + QM_TRY(MOZ_TO_RESULT(aPrincipal->GetBaseDomain(baseDomain))); + + MOZ_ASSERT(!baseDomain.IsEmpty()); + + principalMetadata.mGroup = baseDomain + principalMetadata.mSuffix; + + principalMetadata.mStorageOrigin = principalMetadata.mOrigin; + + principalMetadata.mIsPrivate = aPrincipal->GetPrivateBrowsingId() != 0; + + return principalMetadata; +} + +Result<PrincipalMetadata, nsresult> QuotaManager::GetInfoFromWindow( + nsPIDOMWindowOuter* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + QM_TRY(OkIf(sop), Err(NS_ERROR_FAILURE)); + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + QM_TRY(OkIf(principal), Err(NS_ERROR_FAILURE)); + + return GetInfoFromPrincipal(principal); +} + +// static +Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromPrincipal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (aPrincipal->IsSystemPrincipal()) { + return nsAutoCString{GetOriginForChrome()}; + } + + if (aPrincipal->GetIsNullPrincipal()) { + NS_WARNING("IndexedDB not supported from this principal!"); + return Err(NS_ERROR_FAILURE); + } + + QM_TRY_UNWRAP(const auto origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoCString, aPrincipal, GetOrigin)); + + if (origin.EqualsLiteral(kChromeOrigin)) { + NS_WARNING("Non-chrome principal can't use chrome origin!"); + return Err(NS_ERROR_FAILURE); + } + + return origin; +} + +// static +Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromWindow( + nsPIDOMWindowOuter* aWindow) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + QM_TRY(OkIf(sop), Err(NS_ERROR_FAILURE)); + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + QM_TRY(OkIf(principal), Err(NS_ERROR_FAILURE)); + + QM_TRY_RETURN(GetOriginFromPrincipal(principal)); +} + +// static +PrincipalMetadata QuotaManager::GetInfoForChrome() { + return {{}, + GetOriginForChrome(), + GetOriginForChrome(), + GetOriginForChrome(), + false}; +} + +// static +nsLiteralCString QuotaManager::GetOriginForChrome() { + return nsLiteralCString{kChromeOrigin}; +} + +// static +bool QuotaManager::IsOriginInternal(const nsACString& aOrigin) { + MOZ_ASSERT(!aOrigin.IsEmpty()); + + // The first prompt is not required for these origins. + if (aOrigin.EqualsLiteral(kChromeOrigin) || + StringBeginsWith(aOrigin, nsDependentCString(kAboutHomeOriginPrefix)) || + StringBeginsWith(aOrigin, nsDependentCString(kIndexedDBOriginPrefix)) || + StringBeginsWith(aOrigin, nsDependentCString(kResourceOriginPrefix))) { + return true; + } + + return false; +} + +// static +bool QuotaManager::AreOriginsEqualOnDisk(const nsACString& aOrigin1, + const nsACString& aOrigin2) { + return MakeSanitizedOriginCString(aOrigin1) == + MakeSanitizedOriginCString(aOrigin2); +} + +// static +Result<PrincipalInfo, nsresult> QuotaManager::ParseOrigin( + const nsACString& aOrigin) { + // An origin string either corresponds to a SystemPrincipalInfo or a + // ContentPrincipalInfo, see + // QuotaManager::GetOriginFromValidatedPrincipalInfo. + + if (aOrigin.Equals(kChromeOrigin)) { + return PrincipalInfo{SystemPrincipalInfo{}}; + } + + ContentPrincipalInfo contentPrincipalInfo; + + nsCString originalSuffix; + const OriginParser::ResultType result = OriginParser::ParseOrigin( + MakeSanitizedOriginCString(aOrigin), contentPrincipalInfo.spec(), + &contentPrincipalInfo.attrs(), originalSuffix); + QM_TRY(OkIf(result == OriginParser::ValidOrigin), Err(NS_ERROR_FAILURE)); + + return PrincipalInfo{std::move(contentPrincipalInfo)}; +} + +// static +void QuotaManager::InvalidateQuotaCache() { gInvalidateQuotaCache = true; } + +uint64_t QuotaManager::LockedCollectOriginsForEviction( + uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) { + mQuotaMutex.AssertCurrentThreadOwns(); + + RefPtr<CollectOriginsHelper> helper = + new CollectOriginsHelper(mQuotaMutex, aMinSizeToBeFreed); + + // Unlock while calling out to XPCOM (code behind the dispatch method needs + // to acquire its own lock which can potentially lead to a deadlock and it + // also calls an observer that can do various stuff like IO, so it's better + // to not hold our mutex while that happens). + { + MutexAutoUnlock autoUnlock(mQuotaMutex); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(helper, NS_DISPATCH_NORMAL)); + } + + return helper->BlockAndReturnOriginsForEviction(aLocks); +} + +void QuotaManager::LockedRemoveQuotaForRepository( + PersistenceType aPersistenceType) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) { + auto& pair = iter.Data(); + + if (RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aPersistenceType)) { + groupInfo->LockedRemoveOriginInfos(); + + pair->LockedClearGroupInfo(aPersistenceType); + + if (!pair->LockedHasGroupInfos()) { + iter.Remove(); + } + } + } +} + +void QuotaManager::LockedRemoveQuotaForOrigin( + const OriginMetadata& aOriginMetadata) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + return; + } + + MOZ_ASSERT(pair); + + if (RefPtr<GroupInfo> groupInfo = + pair->LockedGetGroupInfo(aOriginMetadata.mPersistenceType)) { + groupInfo->LockedRemoveOriginInfo(aOriginMetadata.mOrigin); + + if (!groupInfo->LockedHasOriginInfos()) { + pair->LockedClearGroupInfo(aOriginMetadata.mPersistenceType); + + if (!pair->LockedHasGroupInfos()) { + mGroupInfoPairs.Remove(aOriginMetadata.mGroup); + } + } + } +} + +already_AddRefed<GroupInfo> QuotaManager::LockedGetOrCreateGroupInfo( + PersistenceType aPersistenceType, const nsACString& aSuffix, + const nsACString& aGroup) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* const pair = + mGroupInfoPairs.GetOrInsertNew(aGroup, aSuffix, aGroup); + + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (!groupInfo) { + groupInfo = new GroupInfo(pair, aPersistenceType); + pair->LockedSetGroupInfo(aPersistenceType, groupInfo); + } + + return groupInfo.forget(); +} + +already_AddRefed<OriginInfo> QuotaManager::LockedGetOriginInfo( + PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) { + mQuotaMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + GroupInfoPair* pair; + if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) { + RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType); + if (groupInfo) { + return groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin); + } + } + + return nullptr; +} + +template <typename Iterator> +void QuotaManager::MaybeInsertNonPersistedOriginInfos( + Iterator aDest, const RefPtr<GroupInfo>& aTemporaryGroupInfo, + const RefPtr<GroupInfo>& aDefaultGroupInfo, + const RefPtr<GroupInfo>& aPrivateGroupInfo) { + const auto copy = [&aDest](const GroupInfo& groupInfo) { + std::copy_if( + groupInfo.mOriginInfos.cbegin(), groupInfo.mOriginInfos.cend(), aDest, + [](const auto& originInfo) { return !originInfo->LockedPersisted(); }); + }; + + if (aTemporaryGroupInfo) { + MOZ_ASSERT(PERSISTENCE_TYPE_TEMPORARY == + aTemporaryGroupInfo->GetPersistenceType()); + + copy(*aTemporaryGroupInfo); + } + if (aDefaultGroupInfo) { + MOZ_ASSERT(PERSISTENCE_TYPE_DEFAULT == + aDefaultGroupInfo->GetPersistenceType()); + + copy(*aDefaultGroupInfo); + } + if (aPrivateGroupInfo) { + MOZ_ASSERT(PERSISTENCE_TYPE_PRIVATE == + aPrivateGroupInfo->GetPersistenceType()); + copy(*aPrivateGroupInfo); + } +} + +template <typename Collect, typename Pred> +QuotaManager::OriginInfosFlatTraversable +QuotaManager::CollectLRUOriginInfosUntil(Collect&& aCollect, Pred&& aPred) { + OriginInfosFlatTraversable originInfos; + + std::forward<Collect>(aCollect)(MakeBackInserter(originInfos)); + + originInfos.Sort(OriginInfoAccessTimeComparator()); + + const auto foundIt = std::find_if(originInfos.cbegin(), originInfos.cend(), + std::forward<Pred>(aPred)); + + originInfos.TruncateLength(foundIt - originInfos.cbegin()); + + return originInfos; +} + +QuotaManager::OriginInfosNestedTraversable +QuotaManager::GetOriginInfosExceedingGroupLimit() const { + MutexAutoLock lock(mQuotaMutex); + + OriginInfosNestedTraversable originInfos; + + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + uint64_t groupUsage = 0; + + const RefPtr<GroupInfo> temporaryGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY); + if (temporaryGroupInfo) { + groupUsage += temporaryGroupInfo->mUsage; + } + + const RefPtr<GroupInfo> defaultGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT); + if (defaultGroupInfo) { + groupUsage += defaultGroupInfo->mUsage; + } + + const RefPtr<GroupInfo> privateGroupInfo = + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE); + if (privateGroupInfo) { + groupUsage += privateGroupInfo->mUsage; + } + + if (groupUsage > 0) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager, "Shouldn't be null!"); + + if (groupUsage > quotaManager->GetGroupLimit()) { + originInfos.AppendElement(CollectLRUOriginInfosUntil( + [&temporaryGroupInfo, &defaultGroupInfo, + &privateGroupInfo](auto inserter) { + MaybeInsertNonPersistedOriginInfos( + std::move(inserter), temporaryGroupInfo, defaultGroupInfo, + privateGroupInfo); + }, + [&groupUsage, quotaManager](const auto& originInfo) { + groupUsage -= originInfo->LockedUsage(); + + return groupUsage <= quotaManager->GetGroupLimit(); + })); + } + } + } + + return originInfos; +} + +QuotaManager::OriginInfosNestedTraversable +QuotaManager::GetOriginInfosExceedingGlobalLimit() const { + MutexAutoLock lock(mQuotaMutex); + + QuotaManager::OriginInfosNestedTraversable res; + res.AppendElement(CollectLRUOriginInfosUntil( + // XXX The lambda only needs to capture this, but due to Bug 1421435 it + // can't. + [&](auto inserter) { + for (const auto& entry : mGroupInfoPairs) { + const auto& pair = entry.GetData(); + + MOZ_ASSERT(!entry.GetKey().IsEmpty()); + MOZ_ASSERT(pair); + + MaybeInsertNonPersistedOriginInfos( + inserter, pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY), + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT), + pair->LockedGetGroupInfo(PERSISTENCE_TYPE_PRIVATE)); + } + }, + [temporaryStorageUsage = mTemporaryStorageUsage, + temporaryStorageLimit = mTemporaryStorageLimit, + doomedUsage = uint64_t{0}](const auto& originInfo) mutable { + if (temporaryStorageUsage - doomedUsage <= temporaryStorageLimit) { + return true; + } + + doomedUsage += originInfo->LockedUsage(); + return false; + })); + + return res; +} + +void QuotaManager::ClearOrigins( + const OriginInfosNestedTraversable& aDoomedOriginInfos) { + AssertIsOnIOThread(); + + // If we are in shutdown, we could break off early from clearing origins. + // In such cases, we would like to track the ones that were already cleared + // up, such that other essential cleanup could be performed on clearedOrigins. + // clearedOrigins is used in calls to LockedRemoveQuotaForOrigin and + // OriginClearCompleted below. We could have used a collection of OriginInfos + // rather than flattening them to OriginMetadata but groupInfo in OriginInfo + // is just a raw ptr and LockedRemoveQuotaForOrigin might delete groupInfo and + // as a result, we would not be able to get origin persistence type required + // in OriginClearCompleted call after lockedRemoveQuotaForOrigin call. + nsTArray<OriginMetadata> clearedOrigins; + + // XXX Does this need to be done a) in order and/or b) sequentially? + for (const auto& doomedOriginInfo : + Flatten<OriginInfosFlatTraversable::value_type>(aDoomedOriginInfos)) { +#ifdef DEBUG + { + MutexAutoLock lock(mQuotaMutex); + MOZ_ASSERT(!doomedOriginInfo->LockedPersisted()); + } +#endif + + // TODO: We are currently only checking for this flag here which + // means that we cannot break off once we start cleaning an origin. It + // could be better if we could check for shutdown flag while cleaning an + // origin such that we could break off early from the cleaning process if + // we are stuck cleaning on one huge origin. Bug1797098 has been filed to + // track this. + if (QuotaManager::IsShuttingDown()) { + break; + } + + auto originMetadata = doomedOriginInfo->FlattenToOriginMetadata(); + + DeleteOriginDirectory(originMetadata); + + clearedOrigins.AppendElement(std::move(originMetadata)); + } + + { + MutexAutoLock lock(mQuotaMutex); + + for (const auto& clearedOrigin : clearedOrigins) { + LockedRemoveQuotaForOrigin(clearedOrigin); + } + } + + for (const auto& clearedOrigin : clearedOrigins) { + OriginClearCompleted(clearedOrigin.mPersistenceType, clearedOrigin.mOrigin, + Nullable<Client::Type>()); + } +} + +void QuotaManager::CleanupTemporaryStorage() { + AssertIsOnIOThread(); + + // Evicting origins that exceed their group limit also affects the global + // temporary storage usage, so these steps have to be taken sequentially. + // Combining them doesn't seem worth the added complexity. + ClearOrigins(GetOriginInfosExceedingGroupLimit()); + ClearOrigins(GetOriginInfosExceedingGlobalLimit()); + + if (mTemporaryStorageUsage > mTemporaryStorageLimit) { + // If disk space is still low after origin clear, notify storage pressure. + NotifyStoragePressure(mTemporaryStorageUsage); + } +} + +void QuotaManager::DeleteOriginDirectory( + const OriginMetadata& aOriginMetadata) { + QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata), + QM_VOID); + + nsresult rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + NS_ERROR("Failed to remove directory!"); + } +} + +void QuotaManager::FinalizeOriginEviction( + nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks) { + NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!"); + + RefPtr<FinalizeOriginEvictionOp> op = + new FinalizeOriginEvictionOp(mOwningThread, std::move(aLocks)); + + if (IsOnIOThread()) { + op->RunOnIOThreadImmediately(); + } else { + op->Dispatch(); + } +} + +Result<Ok, nsresult> QuotaManager::ArchiveOrigins( + const nsTArray<FullOriginMetadata>& aFullOriginMetadatas) { + AssertIsOnIOThread(); + MOZ_ASSERT(!aFullOriginMetadatas.IsEmpty()); + + QM_TRY_INSPECT(const auto& storageArchivesDir, + QM_NewLocalFile(*mStorageArchivesPath)); + + // Create another subdir, so once we decide to remove all temporary archives, + // we can remove only the subdir and the parent directory can still be used + // for something else or similar in future. Otherwise, we would have to + // figure out a new name for it. + QM_TRY(MOZ_TO_RESULT(storageArchivesDir->Append(u"0"_ns))); + + PRExplodedTime now; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now); + + const auto dateStr = + nsPrintfCString("%04hd-%02" PRId32 "-%02" PRId32, now.tm_year, + now.tm_month + 1, now.tm_mday); + + QM_TRY_INSPECT( + const auto& storageArchiveDir, + CloneFileAndAppend(*storageArchivesDir, NS_ConvertASCIItoUTF16(dateStr))); + + QM_TRY(MOZ_TO_RESULT( + storageArchiveDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700))); + + QM_TRY_INSPECT(const auto& defaultStorageArchiveDir, + CloneFileAndAppend(*storageArchiveDir, + nsLiteralString(DEFAULT_DIRECTORY_NAME))); + + QM_TRY_INSPECT(const auto& temporaryStorageArchiveDir, + CloneFileAndAppend(*storageArchiveDir, + nsLiteralString(TEMPORARY_DIRECTORY_NAME))); + + for (const auto& fullOriginMetadata : aFullOriginMetadatas) { + MOZ_ASSERT( + IsBestEffortPersistenceType(fullOriginMetadata.mPersistenceType)); + + QM_TRY_INSPECT(const auto& directory, + GetOriginDirectory(fullOriginMetadata)); + + // The origin could have been removed, for example due to corruption. + QM_TRY_INSPECT( + const auto& moved, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT( + directory->MoveTo(fullOriginMetadata.mPersistenceType == + PERSISTENCE_TYPE_DEFAULT + ? defaultStorageArchiveDir + : temporaryStorageArchiveDir, + u""_ns)) + .map([](Ok) { return true; }), + // Predicate. + ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), + // Fallback. + ErrToOk<false>)); + + if (moved) { + RemoveQuotaForOrigin(fullOriginMetadata.mPersistenceType, + fullOriginMetadata); + } + } + + return Ok{}; +} + +auto QuotaManager::GetDirectoryLockTable(PersistenceType aPersistenceType) + -> DirectoryLockTable& { + switch (aPersistenceType) { + case PERSISTENCE_TYPE_TEMPORARY: + return mTemporaryDirectoryLockTable; + case PERSISTENCE_TYPE_DEFAULT: + return mDefaultDirectoryLockTable; + case PERSISTENCE_TYPE_PRIVATE: + return mPrivateDirectoryLockTable; + + case PERSISTENCE_TYPE_PERSISTENT: + case PERSISTENCE_TYPE_INVALID: + default: + MOZ_CRASH("Bad persistence type value!"); + } +} + +bool QuotaManager::IsSanitizedOriginValid(const nsACString& aSanitizedOrigin) { + AssertIsOnIOThread(); + + // Do not parse this sanitized origin string, if we already parsed it. + return mValidOrigins.LookupOrInsertWith( + aSanitizedOrigin, [&aSanitizedOrigin] { + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + const auto result = OriginParser::ParseOrigin(aSanitizedOrigin, spec, + &attrs, originalSuffix); + + return result == OriginParser::ValidOrigin; + }); +} + +Result<nsCString, nsresult> QuotaManager::EnsureStorageOriginFromOrigin( + const nsACString& aOrigin) { + MutexAutoLock lock(mQuotaMutex); + + QM_TRY_UNWRAP( + auto storageOrigin, + mOriginToStorageOriginMap.TryLookupOrInsertWith( + aOrigin, [this, &aOrigin]() -> Result<nsCString, nsresult> { + OriginAttributes originAttributes; + + nsCString originNoSuffix; + QM_TRY(MOZ_TO_RESULT( + originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix))); + + nsCOMPtr<nsIURI> uri; + QM_TRY(MOZ_TO_RESULT( + NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(originNoSuffix) + .SetScheme(kUUIDOriginScheme) + .SetHost(NSID_TrimBracketsASCII(nsID::GenerateUUID())) + .SetPort(-1) + .Finalize(uri))); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, OriginAttributes{}); + QM_TRY(MOZ_TO_RESULT(principal)); + + QM_TRY_UNWRAP(auto origin, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoCString, principal, GetOrigin)); + + mStorageOriginToOriginMap.WithEntryHandle( + origin, + [&aOrigin](auto entryHandle) { entryHandle.Insert(aOrigin); }); + + return nsCString(std::move(origin)); + })); + + return nsCString(std::move(storageOrigin)); +} + +Result<nsCString, nsresult> QuotaManager::GetOriginFromStorageOrigin( + const nsACString& aStorageOrigin) { + MutexAutoLock lock(mQuotaMutex); + + auto maybeOrigin = mStorageOriginToOriginMap.MaybeGet(aStorageOrigin); + if (maybeOrigin.isNothing()) { + return Err(NS_ERROR_FAILURE); + } + + return maybeOrigin.ref(); +} + +int64_t QuotaManager::GenerateDirectoryLockId() { + const int64_t directorylockId = mNextDirectoryLockId; + + if (CheckedInt64 result = CheckedInt64(mNextDirectoryLockId) + 1; + result.isValid()) { + mNextDirectoryLockId = result.value(); + } else { + NS_WARNING("Quota manager has run out of ids for directory locks!"); + + // There's very little chance for this to happen given the max size of + // 64 bit integer but if it happens we can just reset mNextDirectoryLockId + // to zero since such old directory locks shouldn't exist anymore. + mNextDirectoryLockId = 0; + } + + // TODO: Maybe add an assertion here to check that there is no existing + // directory lock with given id. + + return directorylockId; +} + +template <typename Func> +auto QuotaManager::ExecuteInitialization(const Initialization aInitialization, + Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return quota::ExecuteInitialization(mInitializationInfo, aInitialization, + std::forward<Func>(aFunc)); +} + +template <typename Func> +auto QuotaManager::ExecuteInitialization(const Initialization aInitialization, + const nsACString& aContext, + Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return quota::ExecuteInitialization(mInitializationInfo, aInitialization, + aContext, std::forward<Func>(aFunc)); +} + +template <typename Func> +auto QuotaManager::ExecuteOriginInitialization( + const nsACString& aOrigin, const OriginInitialization aInitialization, + const nsACString& aContext, Func&& aFunc) + -> std::invoke_result_t<Func, const FirstInitializationAttempt< + Initialization, StringGenerator>&> { + return quota::ExecuteInitialization( + mInitializationInfo.MutableOriginInitializationInfoRef( + aOrigin, CreateIfNonExistent{}), + aInitialization, aContext, std::forward<Func>(aFunc)); +} + +/******************************************************************************* + * Local class implementations + ******************************************************************************/ + +CollectOriginsHelper::CollectOriginsHelper(mozilla::Mutex& aMutex, + uint64_t aMinSizeToBeFreed) + : Runnable("dom::quota::CollectOriginsHelper"), + mMinSizeToBeFreed(aMinSizeToBeFreed), + mMutex(aMutex), + mCondVar(aMutex, "CollectOriginsHelper::mCondVar"), + mSizeToBeFreed(0), + mWaiting(true) { + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + mMutex.AssertCurrentThreadOwns(); +} + +int64_t CollectOriginsHelper::BlockAndReturnOriginsForEviction( + nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) { + MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!"); + mMutex.AssertCurrentThreadOwns(); + + while (mWaiting) { + mCondVar.Wait(); + } + + mLocks.SwapElements(aLocks); + return mSizeToBeFreed; +} + +NS_IMETHODIMP +CollectOriginsHelper::Run() { + AssertIsOnBackgroundThread(); + + QuotaManager* quotaManager = QuotaManager::Get(); + NS_ASSERTION(quotaManager, "Shouldn't be null!"); + + // We use extra stack vars here to avoid race detector warnings (the same + // memory accessed with and without the lock held). + nsTArray<RefPtr<OriginDirectoryLock>> locks; + uint64_t sizeToBeFreed = + quotaManager->CollectOriginsForEviction(mMinSizeToBeFreed, locks); + + MutexAutoLock lock(mMutex); + + NS_ASSERTION(mWaiting, "Huh?!"); + + mLocks.SwapElements(locks); + mSizeToBeFreed = sizeToBeFreed; + mWaiting = false; + mCondVar.Notify(); + + return NS_OK; +} + +/******************************************************************************* + * OriginOperationBase + ******************************************************************************/ + +NS_IMETHODIMP +OriginOperationBase::Run() { + nsresult rv; + + switch (mState) { + case State_Initial: { + rv = Init(); + break; + } + + case State_DirectoryOpenPending: { + rv = DirectoryOpen(); + break; + } + + case State_DirectoryWorkOpen: { + rv = DirectoryWork(); + break; + } + + case State_UnblockingOpen: { + UnblockOpen(); + return NS_OK; + } + + default: + MOZ_CRASH("Bad state!"); + } + + if (NS_WARN_IF(NS_FAILED(rv)) && mState != State_UnblockingOpen) { + Finish(rv); + } + + return NS_OK; +} + +nsresult OriginOperationBase::DoInit(QuotaManager& aQuotaManager) { + AssertIsOnOwningThread(); + + return NS_OK; +} + +nsresult OriginOperationBase::DirectoryOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_DirectoryOpenPending); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE); + + // Must set this before dispatching otherwise we will race with the IO thread. + AdvanceState(); + + QM_TRY(MOZ_TO_RESULT( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), + NS_ERROR_FAILURE); + + return NS_OK; +} + +void OriginOperationBase::Finish(nsresult aResult) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = aResult; + } + + // Must set mState before dispatching otherwise we will race with the main + // thread. + mState = State_UnblockingOpen; + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +nsresult OriginOperationBase::Init() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State_Initial); + + if (QuotaManager::IsShuttingDown()) { + return NS_ERROR_ABORT; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY(MOZ_TO_RESULT(DoInit(*quotaManager))); + + Open(); + + return NS_OK; +} + +nsresult OriginOperationBase::DirectoryWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State_DirectoryWorkOpen); + + QuotaManager* const quotaManager = QuotaManager::Get(); + QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE); + + if (mNeedsStorageInit) { + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized())); + } + + QM_TRY(MOZ_TO_RESULT(DoDirectoryWork(*quotaManager))); + + // Must set mState before dispatching otherwise we will race with the owning + // thread. + AdvanceState(); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); + + return NS_OK; +} + +void FinalizeOriginEvictionOp::Dispatch() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(GetState() == State_Initial); + + SetState(State_DirectoryOpenPending); + + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL)); +} + +void FinalizeOriginEvictionOp::RunOnIOThreadImmediately() { + AssertIsOnIOThread(); + MOZ_ASSERT(GetState() == State_Initial); + + SetState(State_DirectoryWorkOpen); + + MOZ_ALWAYS_SUCCEEDS(this->Run()); +} + +void FinalizeOriginEvictionOp::Open() { MOZ_CRASH("Shouldn't get here!"); } + +nsresult FinalizeOriginEvictionOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("FinalizeOriginEvictionOp::DoDirectoryWork", OTHER); + + for (const auto& lock : mLocks) { + aQuotaManager.OriginClearCompleted( + lock->GetPersistenceType(), lock->Origin(), Nullable<Client::Type>()); + } + + return NS_OK; +} + +void FinalizeOriginEvictionOp::UnblockOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_UnblockingOpen); + +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + mLocks.Clear(); + + AdvanceState(); +} + +NS_IMPL_ISUPPORTS_INHERITED0(NormalOriginOperationBase, Runnable) + +RefPtr<DirectoryLock> NormalOriginOperationBase::CreateDirectoryLock() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(QuotaManager::Get()); + + return QuotaManager::Get()->CreateDirectoryLockInternal( + mPersistenceType, mOriginScope, mClientType, mExclusive); +} + +void NormalOriginOperationBase::Open() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_Initial); + MOZ_ASSERT(QuotaManager::Get()); + + AdvanceState(); + + RefPtr<DirectoryLock> directoryLock = CreateDirectoryLock(); + if (directoryLock) { + directoryLock->Acquire(this); + } else { + QM_TRY(MOZ_TO_RESULT(DirectoryOpen()), QM_VOID, + [this](const nsresult rv) { Finish(rv); }); + } +} + +void NormalOriginOperationBase::UnblockOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_UnblockingOpen); + + SendResults(); + + if (mDirectoryLock) { + mDirectoryLock = nullptr; + } + + UnregisterNormalOriginOp(*this); + + AdvanceState(); +} + +void NormalOriginOperationBase::DirectoryLockAcquired(DirectoryLock* aLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aLock); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + mDirectoryLock = aLock; + + QM_TRY(MOZ_TO_RESULT(DirectoryOpen()), QM_VOID, + [this](const nsresult rv) { Finish(rv); }); +} + +void NormalOriginOperationBase::DirectoryLockFailed() { + AssertIsOnOwningThread(); + MOZ_ASSERT(GetState() == State_DirectoryOpenPending); + MOZ_ASSERT(!mDirectoryLock); + + Finish(NS_ERROR_FAILURE); +} + +nsresult SaveOriginAccessTimeOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + AUTO_PROFILER_LABEL("SaveOriginAccessTimeOp::DoDirectoryWork", OTHER); + + QM_TRY_INSPECT(const auto& file, + aQuotaManager.GetOriginDirectory(mOriginMetadata)); + + // The origin directory might not exist + // anymore, because it was deleted by a clear operation. + QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_FILE_NAME)))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Update)); + MOZ_ASSERT(stream); + + QM_TRY(MOZ_TO_RESULT(stream->Write64(mTimestamp))); + } + + return NS_OK; +} + +void SaveOriginAccessTimeOp::SendResults() { +#ifdef DEBUG + NoteActorDestroyed(); +#endif +} + +nsresult ClearPrivateRepositoryOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_PRIVATE); + + AUTO_PROFILER_LABEL("ClearPrivateRepositoryOp::DoDirectoryWork", OTHER); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(mPersistenceType.Value()))); + + nsresult rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + MOZ_ASSERT(false, "Failed to remove directory!"); + } + + aQuotaManager.RemoveQuotaForRepository(mPersistenceType.Value()); + + aQuotaManager.RepositoryClearCompleted(mPersistenceType.Value()); + + return NS_OK; +} + +void ClearPrivateRepositoryOp::SendResults() { +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + if (NS_SUCCEEDED(mResultCode)) { + mPromiseHolder.ResolveIfExists(true, __func__); + } else { + mPromiseHolder.RejectIfExists(mResultCode, __func__); + } +} + +#ifdef DEBUG +nsresult ShutdownStorageOp::DirectoryOpen() { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDirectoryLock); + mDirectoryLock->AssertIsAcquiredExclusively(); + + return NormalOriginOperationBase::DirectoryOpen(); +} +#endif + +nsresult ShutdownStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ShutdownStorageOp::DoDirectoryWork", OTHER); + + aQuotaManager.ShutdownStorageInternal(); + + return NS_OK; +} + +void ShutdownStorageOp::SendResults() { +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + if (NS_SUCCEEDED(mResultCode)) { + mPromiseHolder.ResolveIfExists(true, __func__); + } else { + mPromiseHolder.RejectIfExists(mResultCode, __func__); + } +} + +NS_IMETHODIMP +StoragePressureRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obsSvc)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISupportsPRUint64> wrapper = + do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID); + if (NS_WARN_IF(!wrapper)) { + return NS_ERROR_FAILURE; + } + + wrapper->SetData(mUsage); + + obsSvc->NotifyObservers(wrapper, "QuotaManager::StoragePressure", u""); + + return NS_OK; +} + +TimeStamp RecordQuotaInfoLoadTimeHelper::Start() { + AssertIsOnIOThread(); + + // XXX: If a OS sleep/wake occur after mStartTime is initialized but before + // gLastOSWake is set, then this time duration would still be recorded with + // key "Normal". We are assumming this is rather rare to happen. + mStartTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return *mStartTime; +} + +TimeStamp RecordQuotaInfoLoadTimeHelper::End() { + AssertIsOnIOThread(); + + mEndTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return *mEndTime; +} + +NS_IMETHODIMP +RecordQuotaInfoLoadTimeHelper::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mInitializedTime.isSome()) { + // Keys for QM_QUOTA_INFO_LOAD_TIME_V0: + // Normal: Normal conditions. + // WasSuspended: There was a OS sleep so that it was suspended. + // TimeStampErr1: The recorded start time is unexpectedly greater than the + // end time. + // TimeStampErr2: The initialized time for the recording class is unexpectly + // greater than the last OS wake time. + const auto key = [this, wasSuspended = gLastOSWake > *mInitializedTime]() { + if (wasSuspended) { + return "WasSuspended"_ns; + } + + // XXX File a bug if we have data for this key. + // We found negative values in our query in STMO for + // ScalarID::QM_REPOSITORIES_INITIALIZATION_TIME. This shouldn't happen + // because the documentation for TimeStamp::Now() says it returns a + // monotonically increasing number. + if (*mStartTime > *mEndTime) { + return "TimeStampErr1"_ns; + } + + if (*mInitializedTime > gLastOSWake) { + return "TimeStampErr2"_ns; + } + + return "Normal"_ns; + }(); + + Telemetry::AccumulateTimeDelta(Telemetry::QM_QUOTA_INFO_LOAD_TIME_V0, key, + *mStartTime, *mEndTime); + + return NS_OK; + } + + gLastOSWake = TimeStamp::Now(); + mInitializedTime.init(gLastOSWake); + + return NS_OK; +} + +/******************************************************************************* + * Quota + ******************************************************************************/ + +Quota::Quota() +#ifdef DEBUG + : mActorDestroyed(false) +#endif +{ +} + +Quota::~Quota() { MOZ_ASSERT(mActorDestroyed); } + +bool Quota::VerifyRequestParams(const UsageRequestParams& aParams) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + + switch (aParams.type()) { + case UsageRequestParams::TAllUsageParams: + break; + + case UsageRequestParams::TOriginUsageParams: { + const OriginUsageParams& params = aParams.get_OriginUsageParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +bool Quota::VerifyRequestParams(const RequestParams& aParams) const { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + switch (aParams.type()) { + case RequestParams::TStorageNameParams: + case RequestParams::TStorageInitializedParams: + case RequestParams::TTemporaryStorageInitializedParams: + case RequestParams::TInitParams: + case RequestParams::TInitTemporaryStorageParams: + break; + + case RequestParams::TInitializePersistentOriginParams: { + const InitializePersistentOriginParams& params = + aParams.get_InitializePersistentOriginParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TInitializeTemporaryOriginParams: { + const InitializeTemporaryOriginParams& params = + aParams.get_InitializeTemporaryOriginParams(); + + if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TGetFullOriginMetadataParams: { + const GetFullOriginMetadataParams& params = + aParams.get_GetFullOriginMetadataParams(); + if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TClearOriginParams: { + const ClearResetOriginParams& params = + aParams.get_ClearOriginParams().commonParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (params.persistenceTypeIsExplicit()) { + if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + if (params.clientTypeIsExplicit()) { + if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + break; + } + + case RequestParams::TResetOriginParams: { + const ClearResetOriginParams& params = + aParams.get_ResetOriginParams().commonParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + if (params.persistenceTypeIsExplicit()) { + if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + if (params.clientTypeIsExplicit()) { + if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + } + + break; + } + + case RequestParams::TClearDataParams: { + if (BackgroundParent::IsOtherProcessActor(Manager())) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TClearPrivateBrowsingParams: + case RequestParams::TClearAllParams: + case RequestParams::TResetAllParams: + case RequestParams::TListOriginsParams: + break; + + case RequestParams::TPersistedParams: { + const PersistedParams& params = aParams.get_PersistedParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TPersistParams: { + const PersistParams& params = aParams.get_PersistParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + case RequestParams::TEstimateParams: { + const EstimateParams& params = aParams.get_EstimateParams(); + + if (NS_WARN_IF( + !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) { + MOZ_CRASH_UNLESS_FUZZING(); + return false; + } + + break; + } + + default: + MOZ_CRASH("Should never get here!"); + } + + return true; +} + +void Quota::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); +#ifdef DEBUG + MOZ_ASSERT(!mActorDestroyed); + mActorDestroyed = true; +#endif +} + +PQuotaUsageRequestParent* Quota::AllocPQuotaUsageRequestParent( + const UsageRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + MOZ_CRASH_UNLESS_FUZZING(); + return nullptr; + } + + QM_TRY(QuotaManager::EnsureCreated(), nullptr); + + auto actor = [&]() -> RefPtr<QuotaUsageRequestBase> { + switch (aParams.type()) { + case UsageRequestParams::TAllUsageParams: + return MakeRefPtr<GetUsageOp>(aParams); + + case UsageRequestParams::TOriginUsageParams: + return MakeRefPtr<GetOriginUsageOp>(aParams); + + default: + MOZ_CRASH("Should never get here!"); + } + }(); + + MOZ_ASSERT(actor); + + RegisterNormalOriginOp(*actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult Quota::RecvPQuotaUsageRequestConstructor( + PQuotaUsageRequestParent* aActor, const UsageRequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + auto* op = static_cast<QuotaUsageRequestBase*>(aActor); + + op->Init(*this); + + op->RunImmediately(); + return IPC_OK(); +} + +bool Quota::DeallocPQuotaUsageRequestParent(PQuotaUsageRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<QuotaUsageRequestBase> actor = + dont_AddRef(static_cast<QuotaUsageRequestBase*>(aActor)); + return true; +} + +PQuotaRequestParent* Quota::AllocPQuotaRequestParent( + const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + + if (NS_WARN_IF(QuotaManager::IsShuttingDown())) { + return nullptr; + } + +#ifdef DEBUG + // Always verify parameters in DEBUG builds! + bool trustParams = false; +#else + bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager()); +#endif + + if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) { + MOZ_CRASH_UNLESS_FUZZING(); + return nullptr; + } + + QM_TRY(QuotaManager::EnsureCreated(), nullptr); + + auto actor = [&]() -> RefPtr<QuotaRequestBase> { + switch (aParams.type()) { + case RequestParams::TStorageNameParams: + return MakeRefPtr<StorageNameOp>(); + + case RequestParams::TStorageInitializedParams: + return MakeRefPtr<StorageInitializedOp>(); + + case RequestParams::TTemporaryStorageInitializedParams: + return MakeRefPtr<TemporaryStorageInitializedOp>(); + + case RequestParams::TInitParams: + return MakeRefPtr<InitOp>(); + + case RequestParams::TInitTemporaryStorageParams: + return MakeRefPtr<InitTemporaryStorageOp>(); + + case RequestParams::TInitializePersistentOriginParams: + return MakeRefPtr<InitializePersistentOriginOp>(aParams); + + case RequestParams::TInitializeTemporaryOriginParams: + return MakeRefPtr<InitializeTemporaryOriginOp>(aParams); + + case RequestParams::TGetFullOriginMetadataParams: + return MakeRefPtr<GetFullOriginMetadataOp>( + aParams.get_GetFullOriginMetadataParams()); + + case RequestParams::TClearOriginParams: + return MakeRefPtr<ClearOriginOp>(aParams); + + case RequestParams::TResetOriginParams: + return MakeRefPtr<ResetOriginOp>(aParams); + + case RequestParams::TClearDataParams: + return MakeRefPtr<ClearDataOp>(aParams); + + case RequestParams::TClearPrivateBrowsingParams: + return MakeRefPtr<ClearPrivateBrowsingOp>(); + + case RequestParams::TClearAllParams: + return MakeRefPtr<ResetOrClearOp>(/* aClear */ true); + + case RequestParams::TResetAllParams: + return MakeRefPtr<ResetOrClearOp>(/* aClear */ false); + + case RequestParams::TPersistedParams: + return MakeRefPtr<PersistedOp>(aParams); + + case RequestParams::TPersistParams: + return MakeRefPtr<PersistOp>(aParams); + + case RequestParams::TEstimateParams: + return MakeRefPtr<EstimateOp>(aParams.get_EstimateParams()); + + case RequestParams::TListOriginsParams: + return MakeRefPtr<ListOriginsOp>(); + + default: + MOZ_CRASH("Should never get here!"); + } + }(); + + MOZ_ASSERT(actor); + + RegisterNormalOriginOp(*actor); + + // Transfer ownership to IPDL. + return actor.forget().take(); +} + +mozilla::ipc::IPCResult Quota::RecvPQuotaRequestConstructor( + PQuotaRequestParent* aActor, const RequestParams& aParams) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(aParams.type() != RequestParams::T__None); + MOZ_ASSERT(!QuotaManager::IsShuttingDown()); + + auto* op = static_cast<QuotaRequestBase*>(aActor); + + op->Init(*this); + + op->RunImmediately(); + return IPC_OK(); +} + +bool Quota::DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aActor); + + // Transfer ownership back from IPDL. + RefPtr<QuotaRequestBase> actor = + dont_AddRef(static_cast<QuotaRequestBase*>(aActor)); + return true; +} + +mozilla::ipc::IPCResult Quota::RecvStartIdleMaintenance() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + MOZ_CRASH_UNLESS_FUZZING(); + return IPC_FAIL(this, "Wrong actor"); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QM_TRY(QuotaManager::EnsureCreated(), IPC_OK()); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->StartIdleMaintenance(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Quota::RecvStopIdleMaintenance() { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + MOZ_CRASH_UNLESS_FUZZING(); + return IPC_FAIL(this, "Wrong actor"); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + return IPC_OK(); + } + + quotaManager->StopIdleMaintenance(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult Quota::RecvAbortOperationsForProcess( + const ContentParentId& aContentParentId) { + AssertIsOnBackgroundThread(); + + PBackgroundParent* actor = Manager(); + MOZ_ASSERT(actor); + + if (BackgroundParent::IsOtherProcessActor(actor)) { + MOZ_CRASH_UNLESS_FUZZING(); + return IPC_FAIL(this, "Wrong actor"); + } + + if (QuotaManager::IsShuttingDown()) { + return IPC_OK(); + } + + QuotaManager* quotaManager = QuotaManager::Get(); + if (!quotaManager) { + return IPC_OK(); + } + + quotaManager->AbortOperationsForProcess(aContentParentId); + + return IPC_OK(); +} + +void QuotaUsageRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsStorageInit = true; +} + +Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOrigin( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); + + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetOriginDirectory(aOriginMetadata)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (!exists || mCanceled) { + return UsageInfo(); + } + + // If the directory exists then enumerate all the files inside, adding up + // the sizes to get the final usage statistic. + bool initialized; + + if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + initialized = aQuotaManager.IsOriginInitialized(aOriginMetadata.mOrigin); + } else { + initialized = aQuotaManager.IsTemporaryStorageInitialized(); + } + + return GetUsageForOriginEntries(aQuotaManager, aPersistenceType, + aOriginMetadata, *directory, initialized); +} + +Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOriginEntries( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType, + const OriginMetadata& aOriginMetadata, nsIFile& aDirectory, + const bool aInitialized) { + AssertIsOnIOThread(); + + QM_TRY_RETURN((ReduceEachFileAtomicCancelable( + aDirectory, mCanceled, UsageInfo{}, + [&](UsageInfo oldUsageInfo, const nsCOMPtr<nsIFile>& file) + -> mozilla::Result<UsageInfo, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName)); + + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + Client::Type clientType; + const bool ok = + Client::TypeFromText(leafName, clientType, fallible); + if (!ok) { + // Unknown directories during getting usage for an origin (even + // for an uninitialized origin) are now allowed. Just warn if we + // find them. + UNKNOWN_FILE_WARNING(leafName); + break; + } + + Client* const client = aQuotaManager.GetClient(clientType); + MOZ_ASSERT(client); + + QM_TRY_INSPECT( + const auto& usageInfo, + aInitialized ? client->GetUsageForOrigin( + aPersistenceType, aOriginMetadata, mCanceled) + : client->InitOrigin(aPersistenceType, + aOriginMetadata, mCanceled)); + return oldUsageInfo + usageInfo; + } + + case nsIFileKind::ExistsAsFile: + // We are maintaining existing behavior for unknown files here (just + // continuing). + // This can possibly be used by developers to add temporary backups + // into origin directories without losing get usage functionality. + if (IsTempMetadata(leafName)) { + if (!aInitialized) { + QM_TRY(MOZ_TO_RESULT(file->Remove(/* recursive */ false))); + } + + break; + } + + if (IsOriginMetadata(leafName) || IsOSMetadata(leafName) || + IsDotFile(leafName)) { + break; + } + + // Unknown files during getting usage for an origin (even for an + // uninitialized origin) are now allowed. Just warn if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return oldUsageInfo; + }))); +} + +void QuotaUsageRequestBase::SendResults() { + AssertIsOnOwningThread(); + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + } else { + if (mCanceled) { + mResultCode = NS_ERROR_FAILURE; + } + + UsageRequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + GetResponse(response); + } else { + response = mResultCode; + } + + Unused << PQuotaUsageRequestParent::Send__delete__(this, response); + } +} + +void QuotaUsageRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +mozilla::ipc::IPCResult QuotaUsageRequestBase::RecvCancel() { + AssertIsOnOwningThread(); + + if (mCanceled.exchange(true)) { + NS_WARNING("Canceled more than once?!"); + return IPC_FAIL(this, "Request canceled more than once"); + } + + return IPC_OK(); +} + +nsresult TraverseRepositoryHelper::TraverseRepository( + QuotaManager& aQuotaManager, PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (!exists) { + return NS_OK; + } + + QM_TRY(CollectEachFileAtomicCancelable( + *directory, GetIsCanceledFlag(), + [this, aPersistenceType, &aQuotaManager, + persistent = aPersistenceType == PERSISTENCE_TYPE_PERSISTENT]( + const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: + QM_TRY(MOZ_TO_RESULT(ProcessOrigin(aQuotaManager, *originDir, + persistent, aPersistenceType))); + break; + + case nsIFileKind::ExistsAsFile: { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, originDir, GetLeafName)); + + // Unknown files during getting usages are allowed. Just warn if we + // find them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + })); + + return NS_OK; +} + +GetUsageOp::GetUsageOp(const UsageRequestParams& aParams) + : QuotaUsageRequestBase("dom::quota::GetUsageOp"), + mGetAll(aParams.get_AllUsageParams().getAll()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == UsageRequestParams::TAllUsageParams); +} + +void GetUsageOp::ProcessOriginInternal(QuotaManager* aQuotaManager, + const PersistenceType aPersistenceType, + const nsACString& aOrigin, + const int64_t aTimestamp, + const bool aPersisted, + const uint64_t aUsage) { + if (!mGetAll && aQuotaManager->IsOriginInternal(aOrigin)) { + return; + } + + // We can't store pointers to OriginUsage objects in the hashtable + // since AppendElement() reallocates its internal array buffer as number + // of elements grows. + const auto& originUsage = + mOriginUsagesIndex.WithEntryHandle(aOrigin, [&](auto&& entry) { + if (entry) { + return WrapNotNullUnchecked(&mOriginUsages[entry.Data()]); + } + + entry.Insert(mOriginUsages.Length()); + + return mOriginUsages.EmplaceBack(nsCString{aOrigin}, false, 0, 0); + }); + + if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) { + originUsage->persisted() = aPersisted; + } + + originUsage->usage() = originUsage->usage() + aUsage; + + originUsage->lastAccessed() = + std::max<int64_t>(originUsage->lastAccessed(), aTimestamp); +} + +const Atomic<bool>& GetUsageOp::GetIsCanceledFlag() { + AssertIsOnIOThread(); + + return mCanceled; +} + +// XXX Remove aPersistent +// XXX Remove aPersistenceType once GetUsageForOrigin uses the persistence +// type from OriginMetadata +nsresult GetUsageOp::ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, const bool aPersistent, + const PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_UNWRAP(auto maybeMetadata, + QM_OR_ELSE_WARN_IF( + // Expression + aQuotaManager.LoadFullOriginMetadataWithRestore(&aOriginDir) + .map([](auto metadata) -> Maybe<FullOriginMetadata> { + return Some(std::move(metadata)); + }), + // Predicate. + IsSpecificError<NS_ERROR_MALFORMED_URI>, + // Fallback. + ErrToDefaultOk<Maybe<FullOriginMetadata>>)); + + if (!maybeMetadata) { + // Unknown directories during getting usage are allowed. Just warn if we + // find them. + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aOriginDir, + GetLeafName)); + + UNKNOWN_FILE_WARNING(leafName); + return NS_OK; + } + + auto metadata = maybeMetadata.extract(); + + QM_TRY_INSPECT(const auto& usageInfo, + GetUsageForOrigin(aQuotaManager, aPersistenceType, metadata)); + + ProcessOriginInternal(&aQuotaManager, aPersistenceType, metadata.mOrigin, + metadata.mLastAccessTime, metadata.mPersisted, + usageInfo.TotalUsage().valueOr(0)); + + return NS_OK; +} + +nsresult GetUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("GetUsageOp::DoDirectoryWork", OTHER); + + nsresult rv; + + for (const PersistenceType type : kAllPersistenceTypes) { + rv = TraverseRepository(aQuotaManager, type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // TraverseRepository above only consulted the filesystem. We also need to + // consider origins which may have pending quota usage, such as buffered + // LocalStorage writes for an origin which didn't previously have any + // LocalStorage data. + + aQuotaManager.CollectPendingOriginsForListing( + [this, &aQuotaManager](const auto& originInfo) { + ProcessOriginInternal( + &aQuotaManager, originInfo->GetGroupInfo()->GetPersistenceType(), + originInfo->Origin(), originInfo->LockedAccessTime(), + originInfo->LockedPersisted(), originInfo->LockedUsage()); + }); + + return NS_OK; +} + +void GetUsageOp::GetResponse(UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = AllUsageResponse(); + + aResponse.get_AllUsageResponse().originUsages() = std::move(mOriginUsages); +} + +GetOriginUsageOp::GetOriginUsageOp(const UsageRequestParams& aParams) + : QuotaUsageRequestBase("dom::quota::GetOriginUsageOp"), + mParams(aParams.get_OriginUsageParams()), + mUsage(0), + mFileUsage(0) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == UsageRequestParams::TOriginUsageParams); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = true; + + // Overwrite GetOriginUsageOp default values. + mFromMemory = mParams.fromMemory(); +} + +nsresult GetOriginUsageOp::DoInit(QuotaManager& aQuotaManager) { + AssertIsOnOwningThread(); + + QM_TRY_UNWRAP( + PrincipalMetadata principalMetadata, + aQuotaManager.GetInfoFromValidatedPrincipalInfo(mParams.principalInfo())); + + principalMetadata.AssertInvariants(); + + mSuffix = std::move(principalMetadata.mSuffix); + mGroup = std::move(principalMetadata.mGroup); + mOriginScope.SetFromOrigin(principalMetadata.mOrigin); + mStorageOrigin = std::move(principalMetadata.mStorageOrigin); + mIsPrivate = principalMetadata.mIsPrivate; + + return NS_OK; +} + +RefPtr<DirectoryLock> GetOriginUsageOp::CreateDirectoryLock() { + if (mFromMemory) { + return nullptr; + } + + return QuotaUsageRequestBase::CreateDirectoryLock(); +} + +nsresult GetOriginUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(mUsage == 0); + MOZ_ASSERT(mFileUsage == 0); + + AUTO_PROFILER_LABEL("GetOriginUsageOp::DoDirectoryWork", OTHER); + + if (mFromMemory) { + const PrincipalMetadata principalMetadata = { + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, mStorageOrigin, + mIsPrivate}; + + // Ensure temporary storage is initialized. If temporary storage hasn't been + // initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including our origin). + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + // Get cached usage (the method doesn't have to stat any files). File usage + // is not tracked in memory separately, so just add to the total usage. + mUsage = aQuotaManager.GetOriginUsage(principalMetadata); + + return NS_OK; + } + + UsageInfo usageInfo; + + // Add all the persistent/temporary/default storage files we care about. + for (const PersistenceType type : kAllPersistenceTypes) { + const OriginMetadata originMetadata = { + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + mStorageOrigin, mIsPrivate, type}; + + auto usageInfoOrErr = + GetUsageForOrigin(aQuotaManager, type, originMetadata); + if (NS_WARN_IF(usageInfoOrErr.isErr())) { + return usageInfoOrErr.unwrapErr(); + } + + usageInfo += usageInfoOrErr.unwrap(); + } + + mUsage = usageInfo.TotalUsage().valueOr(0); + mFileUsage = usageInfo.FileUsage().valueOr(0); + + return NS_OK; +} + +void GetOriginUsageOp::GetResponse(UsageRequestResponse& aResponse) { + AssertIsOnOwningThread(); + + OriginUsageResponse usageResponse; + + usageResponse.usage() = mUsage; + usageResponse.fileUsage() = mFileUsage; + + aResponse = usageResponse; +} + +void QuotaRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsStorageInit = true; +} + +void QuotaRequestBase::SendResults() { + AssertIsOnOwningThread(); + + if (IsActorDestroyed()) { + if (NS_SUCCEEDED(mResultCode)) { + mResultCode = NS_ERROR_FAILURE; + } + } else { + RequestResponse response; + + if (NS_SUCCEEDED(mResultCode)) { + GetResponse(response); + } else { + response = mResultCode; + } + + Unused << PQuotaRequestParent::Send__delete__(this, response); + } +} + +void QuotaRequestBase::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnOwningThread(); + + NoteActorDestroyed(); +} + +StorageNameOp::StorageNameOp() + : QuotaRequestBase("dom::quota::StorageNameOp", /* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; +} + +void StorageNameOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +RefPtr<DirectoryLock> StorageNameOp::CreateDirectoryLock() { return nullptr; } + +nsresult StorageNameOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("StorageNameOp::DoDirectoryWork", OTHER); + + mName = aQuotaManager.GetStorageName(); + + return NS_OK; +} + +void StorageNameOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + StorageNameResponse storageNameResponse; + + storageNameResponse.name() = mName; + + aResponse = storageNameResponse; +} + +InitializedRequestBase::InitializedRequestBase(const char* aRunnableName) + : QuotaRequestBase(aRunnableName, /* aExclusive */ false), + mInitialized(false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; +} + +void InitializedRequestBase::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +RefPtr<DirectoryLock> InitializedRequestBase::CreateDirectoryLock() { + return nullptr; +} + +nsresult StorageInitializedOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("StorageInitializedOp::DoDirectoryWork", OTHER); + + mInitialized = aQuotaManager.IsStorageInitialized(); + + return NS_OK; +} + +void StorageInitializedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + StorageInitializedResponse storageInitializedResponse; + + storageInitializedResponse.initialized() = mInitialized; + + aResponse = storageInitializedResponse; +} + +nsresult TemporaryStorageInitializedOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("TemporaryStorageInitializedOp::DoDirectoryWork", OTHER); + + mInitialized = aQuotaManager.IsTemporaryStorageInitialized(); + + return NS_OK; +} + +void TemporaryStorageInitializedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + TemporaryStorageInitializedResponse temporaryStorageInitializedResponse; + + temporaryStorageInitializedResponse.initialized() = mInitialized; + + aResponse = temporaryStorageInitializedResponse; +} + +InitOp::InitOp() + : QuotaRequestBase("dom::quota::InitOp", /* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; +} + +void InitOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult InitOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("InitOp::DoDirectoryWork", OTHER); + + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureStorageIsInitialized())); + + return NS_OK; +} + +void InitOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitResponse(); +} + +InitTemporaryStorageOp::InitTemporaryStorageOp() + : QuotaRequestBase("dom::quota::InitTemporaryStorageOp", + /* aExclusive */ false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; +} + +void InitTemporaryStorageOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult InitTemporaryStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("InitTemporaryStorageOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + return NS_OK; +} + +void InitTemporaryStorageOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitTemporaryStorageResponse(); +} + +InitializeOriginRequestBase::InitializeOriginRequestBase( + const char* aRunnableName, const PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo) + : QuotaRequestBase(aRunnableName, + /* aExclusive */ false), + mPrincipalInfo(aPrincipalInfo), + mCreated(false) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; + + // Overwrite NormalOriginOperationBase default values. + mPersistenceType.SetValue(aPersistenceType); +} + +void InitializeOriginRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); +} + +nsresult InitializeOriginRequestBase::DoInit(QuotaManager& aQuotaManager) { + AssertIsOnOwningThread(); + + QM_TRY_UNWRAP( + auto principalMetadata, + aQuotaManager.GetInfoFromValidatedPrincipalInfo(mPrincipalInfo)); + + principalMetadata.AssertInvariants(); + + mSuffix = std::move(principalMetadata.mSuffix); + mGroup = std::move(principalMetadata.mGroup); + mOriginScope.SetFromOrigin(principalMetadata.mOrigin); + mStorageOrigin = std::move(principalMetadata.mStorageOrigin); + mIsPrivate = principalMetadata.mIsPrivate; + + return NS_OK; +} + +InitializePersistentOriginOp::InitializePersistentOriginOp( + const RequestParams& aParams) + : InitializeOriginRequestBase( + "dom::quota::InitializePersistentOriginOp", + PERSISTENCE_TYPE_PERSISTENT, + aParams.get_InitializePersistentOriginParams().principalInfo()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == + RequestParams::TInitializePersistentOriginParams); +} + +nsresult InitializePersistentOriginOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + + AUTO_PROFILER_LABEL("InitializePersistentOriginOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY_UNWRAP( + mCreated, + (aQuotaManager + .EnsurePersistentOriginIsInitialized(OriginMetadata{ + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + mStorageOrigin, mIsPrivate, PERSISTENCE_TYPE_PERSISTENT}) + .map([](const auto& res) { return res.second; }))); + + return NS_OK; +} + +void InitializePersistentOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitializePersistentOriginResponse(mCreated); +} + +InitializeTemporaryOriginOp::InitializeTemporaryOriginOp( + const RequestParams& aParams) + : InitializeOriginRequestBase( + "dom::quota::InitializeTemporaryOriginOp", + aParams.get_InitializeTemporaryOriginParams().persistenceType(), + aParams.get_InitializeTemporaryOriginParams().principalInfo()) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TInitializeTemporaryOriginParams); +} + +nsresult InitializeTemporaryOriginOp::DoDirectoryWork( + QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + + AUTO_PROFILER_LABEL("InitializeTemporaryOriginOp::DoDirectoryWork", OTHER); + + QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY(OkIf(aQuotaManager.IsTemporaryStorageInitialized()), NS_ERROR_FAILURE); + + QM_TRY_UNWRAP( + mCreated, + (aQuotaManager + .EnsureTemporaryOriginIsInitialized( + mPersistenceType.Value(), + OriginMetadata{ + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + mStorageOrigin, mIsPrivate, mPersistenceType.Value()}) + .map([](const auto& res) { return res.second; }))); + + return NS_OK; +} + +void InitializeTemporaryOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = InitializeTemporaryOriginResponse(mCreated); +} + +GetFullOriginMetadataOp::GetFullOriginMetadataOp( + const GetFullOriginMetadataParams& aParams) + : QuotaRequestBase("dom::quota::GetFullOriginMetadataOp", + /* aExclusive */ false), + mParams(aParams) { + AssertIsOnOwningThread(); +} + +nsresult GetFullOriginMetadataOp::DoInit(QuotaManager& aQuotaManager) { + AssertIsOnOwningThread(); + + QM_TRY_UNWRAP( + PrincipalMetadata principalMetadata, + aQuotaManager.GetInfoFromValidatedPrincipalInfo(mParams.principalInfo())); + + principalMetadata.AssertInvariants(); + + mOriginMetadata = {std::move(principalMetadata), mParams.persistenceType()}; + + return NS_OK; +} + +RefPtr<DirectoryLock> GetFullOriginMetadataOp::CreateDirectoryLock() { + return nullptr; +} + +nsresult GetFullOriginMetadataOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("GetFullOriginMetadataOp::DoDirectoryWork", OTHER); + + // Ensure temporary storage is initialized. If temporary storage hasn't + // been initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including our origin). + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + // Get metadata cached in memory (the method doesn't have to stat any + // files). + mMaybeFullOriginMetadata = + aQuotaManager.GetFullOriginMetadata(mOriginMetadata); + + return NS_OK; +} + +void GetFullOriginMetadataOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = GetFullOriginMetadataResponse(); + aResponse.get_GetFullOriginMetadataResponse().maybeFullOriginMetadata() = + std::move(mMaybeFullOriginMetadata); +} + +ResetOrClearOp::ResetOrClearOp(bool aClear) + : QuotaRequestBase("dom::quota::ResetOrClearOp", /* aExclusive */ true), + mClear(aClear) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; +} + +void ResetOrClearOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +void ResetOrClearOp::DeleteFiles(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + nsresult rv = aQuotaManager.AboutToClearOrigins(Nullable<PersistenceType>(), + OriginScope::FromNull(), + Nullable<Client::Type>()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + auto directoryOrErr = QM_NewLocalFile(aQuotaManager.GetStoragePath()); + if (NS_WARN_IF(directoryOrErr.isErr())) { + return; + } + + nsCOMPtr<nsIFile> directory = directoryOrErr.unwrap(); + + rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + MOZ_ASSERT(false, "Failed to remove storage directory!"); + } +} + +void ResetOrClearOp::DeleteStorageFile(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& storageFile, + QM_NewLocalFile(aQuotaManager.GetBasePath()), QM_VOID); + + QM_TRY(MOZ_TO_RESULT(storageFile->Append(aQuotaManager.GetStorageName() + + kSQLiteSuffix)), + QM_VOID); + + const nsresult rv = storageFile->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed the storage connection + // correctly... + MOZ_ASSERT(false, "Failed to remove storage file!"); + } +} + +nsresult ResetOrClearOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ResetOrClearOp::DoDirectoryWork", OTHER); + + if (mClear) { + DeleteFiles(aQuotaManager); + + aQuotaManager.RemoveQuota(); + } + + aQuotaManager.ShutdownStorageInternal(); + + if (mClear) { + DeleteStorageFile(aQuotaManager); + } + + return NS_OK; +} + +void ResetOrClearOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + if (mClear) { + aResponse = ClearAllResponse(); + } else { + aResponse = ResetAllResponse(); + } +} + +ClearPrivateBrowsingOp::ClearPrivateBrowsingOp() + : QuotaRequestBase("dom::quota::ClearPrivateBrowsingOp", + Nullable<PersistenceType>(PERSISTENCE_TYPE_PRIVATE), + OriginScope::FromNull(), Nullable<Client::Type>(), + /* aExclusive */ true) { + AssertIsOnOwningThread(); +} + +nsresult ClearPrivateBrowsingOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_PRIVATE); + + AUTO_PROFILER_LABEL("ClearPrivateBrowsingOp::DoDirectoryWork", OTHER); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(mPersistenceType.Value()))); + + nsresult rv = directory->Remove(true); + if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) { + // This should never fail if we've closed all storage connections + // correctly... + MOZ_ASSERT(false, "Failed to remove directory!"); + } + + aQuotaManager.RemoveQuotaForRepository(mPersistenceType.Value()); + + aQuotaManager.RepositoryClearCompleted(mPersistenceType.Value()); + + return NS_OK; +} + +void ClearPrivateBrowsingOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearPrivateBrowsingResponse(); +} + +static Result<nsCOMPtr<nsIFile>, QMResult> OpenToBeRemovedDirectory( + const nsAString& aStoragePath) { + QM_TRY_INSPECT(const auto& dir, + QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aStoragePath))); + QM_TRY(QM_TO_RESULT(dir->Append(u"to-be-removed"_ns))); + + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) { + return dir; + } + return Err(QMResult(rv)); +} + +static Result<Ok, QMResult> RemoveOrMoveToDir(nsIFile& aFile, + nsIFile* aMoveTargetDir) { + if (!aMoveTargetDir) { + QM_TRY(QM_TO_RESULT(aFile.Remove(true))); + return Ok(); + } + + nsIDToCString uuid(nsID::GenerateUUID()); + NS_ConvertUTF8toUTF16 subDirName(uuid.get(), NSID_LENGTH - 1); + QM_TRY(QM_TO_RESULT(aFile.MoveTo(aMoveTargetDir, subDirName))); + return Ok(); +} + +void ClearRequestBase::DeleteFiles(QuotaManager& aQuotaManager, + PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY(MOZ_TO_RESULT(aQuotaManager.AboutToClearOrigins( + Nullable<PersistenceType>(aPersistenceType), mOriginScope, + mClientType)), + QM_VOID); + + QM_TRY_INSPECT( + const auto& directory, + QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType)), QM_VOID); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists), QM_VOID); + + if (!exists) { + return; + } + + nsTArray<nsCOMPtr<nsIFile>> directoriesForRemovalRetry; + + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Starting deleting files"_ns); + nsCOMPtr<nsIFile> toBeRemovedDir; + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownTeardown)) { + QM_WARNONLY_TRY_UNWRAP( + auto result, OpenToBeRemovedDirectory(aQuotaManager.GetStoragePath())); + toBeRemovedDir = result.valueOr(nullptr); + } + QM_TRY( + CollectEachFile( + *directory, + [&originScope = mOriginScope, aPersistenceType, &aQuotaManager, + &directoriesForRemovalRetry, &toBeRemovedDir, + this](nsCOMPtr<nsIFile>&& file) -> mozilla::Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); + + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, + GetLeafName)); + + switch (dirEntryKind) { + case nsIFileKind::ExistsAsDirectory: { + QM_TRY_UNWRAP( + auto maybeMetadata, + QM_OR_ELSE_WARN_IF( + // Expression + aQuotaManager.GetOriginMetadata(file).map( + [](auto metadata) -> Maybe<OriginMetadata> { + return Some(std::move(metadata)); + }), + // Predicate. + IsSpecificError<NS_ERROR_MALFORMED_URI>, + // Fallback. + ErrToDefaultOk<Maybe<OriginMetadata>>)); + + if (!maybeMetadata) { + // Unknown directories during clearing are allowed. Just warn + // if we find them. + UNKNOWN_FILE_WARNING(leafName); + break; + } + + auto metadata = maybeMetadata.extract(); + + MOZ_ASSERT(metadata.mPersistenceType == aPersistenceType); + + // Skip the origin directory if it doesn't match the pattern. + if (!originScope.Matches( + OriginScope::FromOrigin(metadata.mOrigin))) { + break; + } + + if (!mClientType.IsNull()) { + nsAutoString clientDirectoryName; + QM_TRY( + OkIf(Client::TypeToText(mClientType.Value(), + clientDirectoryName, fallible)), + Err(NS_ERROR_FAILURE)); + + QM_TRY(MOZ_TO_RESULT(file->Append(clientDirectoryName))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (!exists) { + break; + } + } + + // We can't guarantee that this will always succeed on + // Windows... + QM_WARNONLY_TRY( + RemoveOrMoveToDir(*file, toBeRemovedDir), [&](const auto&) { + directoriesForRemovalRetry.AppendElement(std::move(file)); + }); + + const bool initialized = + aPersistenceType == PERSISTENCE_TYPE_PERSISTENT + ? aQuotaManager.IsOriginInitialized(metadata.mOrigin) + : aQuotaManager.IsTemporaryStorageInitialized(); + + // If it hasn't been initialized, we don't need to update the + // quota and notify the removing client. + if (!initialized) { + break; + } + + if (aPersistenceType != PERSISTENCE_TYPE_PERSISTENT) { + if (mClientType.IsNull()) { + aQuotaManager.RemoveQuotaForOrigin(aPersistenceType, + metadata); + } else { + aQuotaManager.ResetUsageForClient( + ClientMetadata{metadata, mClientType.Value()}); + } + } + + aQuotaManager.OriginClearCompleted( + aPersistenceType, metadata.mOrigin, mClientType); + + break; + } + + case nsIFileKind::ExistsAsFile: { + // Unknown files during clearing are allowed. Just warn if we + // find them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + break; + } + + case nsIFileKind::DoesNotExist: + // Ignore files that got removed externally while iterating. + break; + } + + return Ok{}; + }), + QM_VOID); + + // Retry removing any directories that failed to be removed earlier now. + // + // XXX This will still block this operation. We might instead dispatch a + // runnable to our own thread for each retry round with a timer. We must + // ensure that the directory lock is upheld until we complete or give up + // though. + for (uint32_t index = 0; index < 10; index++) { + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString( + "ClearRequestBase: Starting repeated directory removal #%d", index); + }); + + for (auto&& file : std::exchange(directoriesForRemovalRetry, + nsTArray<nsCOMPtr<nsIFile>>{})) { + QM_WARNONLY_TRY( + QM_TO_RESULT(file->Remove(true)), + ([&directoriesForRemovalRetry, &file](const auto&) { + directoriesForRemovalRetry.AppendElement(std::move(file)); + })); + } + + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString( + "ClearRequestBase: Completed repeated directory removal #%d", index); + }); + + if (directoriesForRemovalRetry.IsEmpty()) { + break; + } + + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString("ClearRequestBase: Before sleep #%d", index); + }); + + PR_Sleep(PR_MillisecondsToInterval(200)); + + aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() { + return nsPrintfCString("ClearRequestBase: After sleep #%d", index); + }); + } + + QM_WARNONLY_TRY(OkIf(directoriesForRemovalRetry.IsEmpty())); + + aQuotaManager.MaybeRecordQuotaManagerShutdownStep( + "ClearRequestBase: Completed deleting files"_ns); +} + +nsresult ClearRequestBase::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("ClearRequestBase::DoDirectoryWork", OTHER); + + if (mPersistenceType.IsNull()) { + for (const PersistenceType type : kAllPersistenceTypes) { + DeleteFiles(aQuotaManager, type); + } + } else { + DeleteFiles(aQuotaManager, mPersistenceType.Value()); + } + + return NS_OK; +} + +ClearOriginOp::ClearOriginOp(const RequestParams& aParams) + : ClearRequestBase("dom::quota::ClearOriginOp", /* aExclusive */ true), + mParams(aParams.get_ClearOriginParams().commonParams()), + mMatchAll(aParams.get_ClearOriginParams().matchAll()) { + MOZ_ASSERT(aParams.type() == RequestParams::TClearOriginParams); +} + +void ClearOriginOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + if (mParams.persistenceTypeIsExplicit()) { + mPersistenceType.SetValue(mParams.persistenceType()); + } + + // Figure out which origin we're dealing with. + const auto origin = QuotaManager::GetOriginFromValidatedPrincipalInfo( + mParams.principalInfo()); + + if (mMatchAll) { + mOriginScope.SetFromPrefix(origin); + } else { + mOriginScope.SetFromOrigin(origin); + } + + if (mParams.clientTypeIsExplicit()) { + mClientType.SetValue(mParams.clientType()); + } +} + +void ClearOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearOriginResponse(); +} + +ClearDataOp::ClearDataOp(const RequestParams& aParams) + : ClearRequestBase("dom::quota::ClearDataOp", /* aExclusive */ true), + mParams(aParams) { + MOZ_ASSERT(aParams.type() == RequestParams::TClearDataParams); +} + +void ClearDataOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + mOriginScope.SetFromPattern(mParams.pattern()); +} + +void ClearDataOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ClearDataResponse(); +} + +ResetOriginOp::ResetOriginOp(const RequestParams& aParams) + : QuotaRequestBase("dom::quota::ResetOriginOp", /* aExclusive */ true) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aParams.type() == RequestParams::TResetOriginParams); + + const ClearResetOriginParams& params = + aParams.get_ResetOriginParams().commonParams(); + + const auto origin = + QuotaManager::GetOriginFromValidatedPrincipalInfo(params.principalInfo()); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = false; + + // Overwrite NormalOriginOperationBase default values. + if (params.persistenceTypeIsExplicit()) { + mPersistenceType.SetValue(params.persistenceType()); + } + + mOriginScope.SetFromOrigin(origin); + + if (params.clientTypeIsExplicit()) { + mClientType.SetValue(params.clientType()); + } +} + +void ResetOriginOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); } + +nsresult ResetOriginOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + + AUTO_PROFILER_LABEL("ResetOriginOp::DoDirectoryWork", OTHER); + + // All the work is handled by NormalOriginOperationBase parent class. In this + // particular case, we just needed to acquire an exclusive directory lock and + // that's it. + + return NS_OK; +} + +void ResetOriginOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ResetOriginResponse(); +} + +PersistRequestBase::PersistRequestBase(const PrincipalInfo& aPrincipalInfo) + : QuotaRequestBase("dom::quota::PersistRequestBase", + /* aExclusive */ false), + mPrincipalInfo(aPrincipalInfo) { + AssertIsOnOwningThread(); +} + +void PersistRequestBase::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + QuotaRequestBase::Init(aQuota); + + mPersistenceType.SetValue(PERSISTENCE_TYPE_DEFAULT); +} + +nsresult PersistRequestBase::DoInit(QuotaManager& aQuotaManager) { + AssertIsOnOwningThread(); + + // Figure out which origin we're dealing with. + QM_TRY_UNWRAP( + PrincipalMetadata principalMetadata, + aQuotaManager.GetInfoFromValidatedPrincipalInfo(mPrincipalInfo)); + + principalMetadata.AssertInvariants(); + + mSuffix = std::move(principalMetadata.mSuffix); + mGroup = std::move(principalMetadata.mGroup); + mOriginScope.SetFromOrigin(principalMetadata.mOrigin); + mStorageOrigin = std::move(principalMetadata.mStorageOrigin); + mIsPrivate = principalMetadata.mIsPrivate; + + return NS_OK; +} + +PersistedOp::PersistedOp(const RequestParams& aParams) + : PersistRequestBase(aParams.get_PersistedParams().principalInfo()), + mPersisted(false) { + MOZ_ASSERT(aParams.type() == RequestParams::TPersistedParams); +} + +nsresult PersistedOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + AUTO_PROFILER_LABEL("PersistedOp::DoDirectoryWork", OTHER); + + const OriginMetadata originMetadata = { + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + mStorageOrigin, mIsPrivate, mPersistenceType.Value()}; + + Nullable<bool> persisted = aQuotaManager.OriginPersisted(originMetadata); + + if (!persisted.IsNull()) { + mPersisted = persisted.Value(); + return NS_OK; + } + + // If we get here, it means the origin hasn't been initialized yet. + // Try to get the persisted flag from directory metadata on disk. + + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetOriginDirectory(originMetadata)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); + + if (exists) { + // Get the metadata. We only use the persisted flag. + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(directory)); + + mPersisted = metadata.mPersisted; + } else { + // The directory has not been created yet. + mPersisted = false; + } + + return NS_OK; +} + +void PersistedOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + PersistedResponse persistedResponse; + persistedResponse.persisted() = mPersisted; + + aResponse = persistedResponse; +} + +PersistOp::PersistOp(const RequestParams& aParams) + : PersistRequestBase(aParams.get_PersistParams().principalInfo()) { + MOZ_ASSERT(aParams.type() == RequestParams::TPersistParams); +} + +nsresult PersistOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + MOZ_ASSERT(!mPersistenceType.IsNull()); + MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT); + MOZ_ASSERT(mOriginScope.IsOrigin()); + + const OriginMetadata originMetadata = { + mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, + mStorageOrigin, mIsPrivate, mPersistenceType.Value()}; + + AUTO_PROFILER_LABEL("PersistOp::DoDirectoryWork", OTHER); + + // Update directory metadata on disk first. Then, create/update the originInfo + // if needed. + QM_TRY_INSPECT(const auto& directory, + aQuotaManager.GetOriginDirectory(originMetadata)); + + QM_TRY_INSPECT(const bool& created, + aQuotaManager.EnsureOriginDirectory(*directory)); + + if (created) { + int64_t timestamp; + + // Origin directory has been successfully created. + // Create OriginInfo too if temporary storage was already initialized. + if (aQuotaManager.IsTemporaryStorageInitialized()) { + timestamp = aQuotaManager.NoteOriginDirectoryCreated( + originMetadata, /* aPersisted */ true); + } else { + timestamp = PR_Now(); + } + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp, + /* aPersisted */ true, + originMetadata))); + } else { + // Get the metadata (restore the metadata file if necessary). We only use + // the persisted flag. + QM_TRY_INSPECT(const auto& metadata, + aQuotaManager.LoadFullOriginMetadataWithRestore(directory)); + + if (!metadata.mPersisted) { + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend( + *directory, nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Update)); + + MOZ_ASSERT(stream); + + // Update origin access time while we are here. + QM_TRY(MOZ_TO_RESULT(stream->Write64(PR_Now()))); + + // Set the persisted flag to true. + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(true))); + } + + // Directory metadata has been successfully updated. + // Update OriginInfo too if temporary storage was already initialized. + if (aQuotaManager.IsTemporaryStorageInitialized()) { + aQuotaManager.PersistOrigin(originMetadata); + } + } + + return NS_OK; +} + +void PersistOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = PersistResponse(); +} + +EstimateOp::EstimateOp(const EstimateParams& aParams) + : QuotaRequestBase("dom::quota::EstimateOp", /* aExclusive */ false), + mParams(aParams) { + AssertIsOnOwningThread(); + + // Overwrite OriginOperationBase default values. + mNeedsStorageInit = true; +} + +nsresult EstimateOp::DoInit(QuotaManager& aQuotaManager) { + AssertIsOnOwningThread(); + + QM_TRY_UNWRAP( + PrincipalMetadata principalMetadata, + aQuotaManager.GetInfoFromValidatedPrincipalInfo(mParams.principalInfo())); + + principalMetadata.AssertInvariants(); + + mOriginMetadata = {std::move(principalMetadata), PERSISTENCE_TYPE_DEFAULT}; + + return NS_OK; +} + +RefPtr<DirectoryLock> EstimateOp::CreateDirectoryLock() { return nullptr; } + +nsresult EstimateOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("EstimateOp::DoDirectoryWork", OTHER); + + // Ensure temporary storage is initialized. If temporary storage hasn't been + // initialized yet, the method will initialize it by traversing the + // repositories for temporary and default storage (including origins belonging + // to our group). + QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized())); + + // Get cached usage (the method doesn't have to stat any files). + mUsageAndLimit = aQuotaManager.GetUsageAndLimitForEstimate(mOriginMetadata); + + return NS_OK; +} + +void EstimateOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + EstimateResponse estimateResponse; + + estimateResponse.usage() = mUsageAndLimit.first; + estimateResponse.limit() = mUsageAndLimit.second; + + aResponse = estimateResponse; +} + +ListOriginsOp::ListOriginsOp() + : QuotaRequestBase("dom::quota::ListOriginsOp", /* aExclusive */ false), + TraverseRepositoryHelper() { + AssertIsOnOwningThread(); +} + +void ListOriginsOp::Init(Quota& aQuota) { + AssertIsOnOwningThread(); + + mNeedsStorageInit = true; +} + +nsresult ListOriginsOp::DoDirectoryWork(QuotaManager& aQuotaManager) { + AssertIsOnIOThread(); + aQuotaManager.AssertStorageIsInitialized(); + + AUTO_PROFILER_LABEL("ListOriginsOp::DoDirectoryWork", OTHER); + + for (const PersistenceType type : kAllPersistenceTypes) { + QM_TRY(MOZ_TO_RESULT(TraverseRepository(aQuotaManager, type))); + } + + // TraverseRepository above only consulted the file-system to get a list of + // known origins, but we also need to include origins that have pending quota + // usage. + + aQuotaManager.CollectPendingOriginsForListing([this](const auto& originInfo) { + mOrigins.AppendElement(originInfo->Origin()); + }); + + return NS_OK; +} + +const Atomic<bool>& ListOriginsOp::GetIsCanceledFlag() { + AssertIsOnIOThread(); + + return mCanceled; +} + +nsresult ListOriginsOp::ProcessOrigin(QuotaManager& aQuotaManager, + nsIFile& aOriginDir, + const bool aPersistent, + const PersistenceType aPersistenceType) { + AssertIsOnIOThread(); + + QM_TRY_UNWRAP(auto maybeMetadata, + QM_OR_ELSE_WARN_IF( + // Expression + aQuotaManager.GetOriginMetadata(&aOriginDir) + .map([](auto metadata) -> Maybe<OriginMetadata> { + return Some(std::move(metadata)); + }), + // Predicate. + IsSpecificError<NS_ERROR_MALFORMED_URI>, + // Fallback. + ErrToDefaultOk<Maybe<OriginMetadata>>)); + + if (!maybeMetadata) { + // Unknown directories during listing are allowed. Just warn if we find + // them. + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aOriginDir, + GetLeafName)); + + UNKNOWN_FILE_WARNING(leafName); + return NS_OK; + } + + auto metadata = maybeMetadata.extract(); + + if (aQuotaManager.IsOriginInternal(metadata.mOrigin)) { + return NS_OK; + } + + mOrigins.AppendElement(std::move(metadata.mOrigin)); + + return NS_OK; +} + +void ListOriginsOp::GetResponse(RequestResponse& aResponse) { + AssertIsOnOwningThread(); + + aResponse = ListOriginsResponse(); + if (mOrigins.IsEmpty()) { + return; + } + + nsTArray<nsCString>& origins = aResponse.get_ListOriginsResponse().origins(); + mOrigins.SwapElements(origins); +} + +nsresult StorageOperationBase::GetDirectoryMetadata(nsIFile* aDirectory, + int64_t& aTimestamp, + nsACString& aGroup, + nsACString& aOrigin, + Nullable<bool>& aIsApp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT( + const auto& binaryStream, + GetBinaryInputStream(*aDirectory, nsLiteralString(METADATA_FILE_NAME))); + + QM_TRY_INSPECT(const uint64_t& timestamp, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + Nullable<bool> isApp; + bool value; + if (NS_SUCCEEDED(binaryStream->ReadBoolean(&value))) { + isApp.SetValue(value); + } + + aTimestamp = timestamp; + aGroup = group; + aOrigin = origin; + aIsApp = std::move(isApp); + return NS_OK; +} + +nsresult StorageOperationBase::GetDirectoryMetadata2( + nsIFile* aDirectory, int64_t& aTimestamp, nsACString& aSuffix, + nsACString& aGroup, nsACString& aOrigin, bool& aIsApp) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT(const auto& binaryStream, + GetBinaryInputStream(*aDirectory, + nsLiteralString(METADATA_V2_FILE_NAME))); + + QM_TRY_INSPECT(const uint64_t& timestamp, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64)); + + QM_TRY_INSPECT(const bool& persisted, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + Unused << persisted; + + QM_TRY_INSPECT(const bool& reservedData1, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData1; + + QM_TRY_INSPECT(const bool& reservedData2, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32)); + Unused << reservedData2; + + QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCString, binaryStream, ReadCString)); + + QM_TRY_INSPECT(const bool& isApp, + MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean)); + + aTimestamp = timestamp; + aSuffix = suffix; + aGroup = group; + aOrigin = origin; + aIsApp = isApp; + return NS_OK; +} + +int64_t StorageOperationBase::GetOriginLastModifiedTime( + const OriginProps& aOriginProps) { + return GetLastModifiedTime(*aOriginProps.mPersistenceType, + *aOriginProps.mDirectory); +} + +nsresult StorageOperationBase::RemoveObsoleteOrigin( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + QM_WARNING( + "Deleting obsolete %s directory that is no longer a legal " + "origin!", + NS_ConvertUTF16toUTF8(aOriginProps.mLeafName).get()); + + QM_TRY(MOZ_TO_RESULT(aOriginProps.mDirectory->Remove(/* recursive */ true))); + + return NS_OK; +} + +Result<bool, nsresult> StorageOperationBase::MaybeRenameOrigin( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + const nsAString& oldLeafName = aOriginProps.mLeafName; + + const auto newLeafName = + MakeSanitizedOriginString(aOriginProps.mOriginMetadata.mOrigin); + + if (oldLeafName == newLeafName) { + return false; + } + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(*aOriginProps.mDirectory, + aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + QM_TRY_INSPECT(const auto& newFile, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, GetParent)); + + QM_TRY(MOZ_TO_RESULT(newFile->Append(newLeafName))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(newFile, Exists)); + + if (exists) { + QM_WARNING( + "Can't rename %s directory to %s, the target already exists, removing " + "instead of renaming!", + NS_ConvertUTF16toUTF8(oldLeafName).get(), + NS_ConvertUTF16toUTF8(newLeafName).get()); + } + + QM_TRY(CallWithDelayedRetriesIfAccessDenied( + [&exists, &aOriginProps, &newLeafName] { + if (exists) { + QM_TRY_RETURN(MOZ_TO_RESULT( + aOriginProps.mDirectory->Remove(/* recursive */ true))); + } + QM_TRY_RETURN(MOZ_TO_RESULT( + aOriginProps.mDirectory->RenameTo(nullptr, newLeafName))); + }, + StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_maxRetries(), + StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_delayMs())); + + return true; +} + +nsresult StorageOperationBase::ProcessOriginDirectories() { + AssertIsOnIOThread(); + MOZ_ASSERT(!mOriginProps.IsEmpty()); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + for (auto& originProps : mOriginProps) { + switch (originProps.mType) { + case OriginProps::eChrome: { + originProps.mOriginMetadata = {QuotaManager::GetInfoForChrome(), + *originProps.mPersistenceType}; + break; + } + + case OriginProps::eContent: { + nsCOMPtr<nsIURI> uri; + QM_TRY( + MOZ_TO_RESULT(NS_NewURI(getter_AddRefs(uri), originProps.mSpec))); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, originProps.mAttrs); + QM_TRY(MOZ_TO_RESULT(principal)); + + PrincipalInfo principalInfo; + QM_TRY( + MOZ_TO_RESULT(PrincipalToPrincipalInfo(principal, &principalInfo))); + + QM_WARNONLY_TRY_UNWRAP( + auto valid, + MOZ_TO_RESULT(quotaManager->IsPrincipalInfoValid(principalInfo))); + + if (!valid) { + // Unknown directories during upgrade are allowed. Just warn if we + // find them. + UNKNOWN_FILE_WARNING(originProps.mLeafName); + originProps.mIgnore = true; + break; + } + + QM_TRY_UNWRAP( + auto principalMetadata, + quotaManager->GetInfoFromValidatedPrincipalInfo(principalInfo)); + + originProps.mOriginMetadata = {std::move(principalMetadata), + *originProps.mPersistenceType}; + + break; + } + + case OriginProps::eObsolete: { + // There's no way to get info for obsolete origins. + break; + } + + default: + MOZ_CRASH("Bad type!"); + } + } + + // Don't try to upgrade obsolete origins, remove them right after we detect + // them. + for (const auto& originProps : mOriginProps) { + if (originProps.mType == OriginProps::eObsolete) { + MOZ_ASSERT(originProps.mOriginMetadata.mSuffix.IsEmpty()); + MOZ_ASSERT(originProps.mOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(originProps.mOriginMetadata.mOrigin.IsEmpty()); + + QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(originProps))); + } else if (!originProps.mIgnore) { + MOZ_ASSERT(!originProps.mOriginMetadata.mGroup.IsEmpty()); + MOZ_ASSERT(!originProps.mOriginMetadata.mOrigin.IsEmpty()); + + QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectory(originProps))); + } + } + + return NS_OK; +} + +// XXX Do the fallible initialization in a separate non-static member function +// of StorageOperationBase and eventually get rid of this method and use a +// normal constructor instead. +template <typename PersistenceTypeFunc> +nsresult StorageOperationBase::OriginProps::Init( + PersistenceTypeFunc&& aPersistenceTypeFunc) { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, *mDirectory, + GetLeafName)); + + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + OriginParser::ResultType result = OriginParser::ParseOrigin( + NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix); + if (NS_WARN_IF(result == OriginParser::InvalidOrigin)) { + mType = OriginProps::eInvalid; + return NS_OK; + } + + const auto persistenceType = [&]() -> PersistenceType { + // XXX We shouldn't continue with initialization if OriginParser returned + // anything else but ValidOrigin. Otherwise, we have to deal with empty + // spec when the origin is obsolete, like here. The caller should handle + // the errors. Until it's fixed, we have to treat obsolete origins as + // origins with unknown/invalid persistence type. + if (result != OriginParser::ValidOrigin) { + return PERSISTENCE_TYPE_INVALID; + } + return std::forward<PersistenceTypeFunc>(aPersistenceTypeFunc)(spec); + }(); + + mLeafName = leafName; + mSpec = spec; + mAttrs = attrs; + mOriginalSuffix = originalSuffix; + mPersistenceType.init(persistenceType); + if (result == OriginParser::ObsoleteOrigin) { + mType = eObsolete; + } else if (mSpec.EqualsLiteral(kChromeOrigin)) { + mType = eChrome; + } else { + mType = eContent; + } + + return NS_OK; +} + +// static +auto OriginParser::ParseOrigin(const nsACString& aOrigin, nsCString& aSpec, + OriginAttributes* aAttrs, + nsCString& aOriginalSuffix) -> ResultType { + MOZ_ASSERT(!aOrigin.IsEmpty()); + MOZ_ASSERT(aAttrs); + + nsCString origin(aOrigin); + int32_t pos = origin.RFindChar('^'); + + if (pos == kNotFound) { + aOriginalSuffix.Truncate(); + } else { + aOriginalSuffix = Substring(origin, pos); + } + + OriginAttributes originAttributes; + + nsCString originNoSuffix; + bool ok = originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix); + if (!ok) { + return InvalidOrigin; + } + + OriginParser parser(originNoSuffix); + + *aAttrs = originAttributes; + return parser.Parse(aSpec); +} + +auto OriginParser::Parse(nsACString& aSpec) -> ResultType { + while (mTokenizer.hasMoreTokens()) { + const nsDependentCSubstring& token = mTokenizer.nextToken(); + + HandleToken(token); + + if (mError) { + break; + } + + if (!mHandledTokens.IsEmpty()) { + mHandledTokens.AppendLiteral(", "); + } + mHandledTokens.Append('\''); + mHandledTokens.Append(token); + mHandledTokens.Append('\''); + } + + if (!mError && mTokenizer.separatorAfterCurrentToken()) { + HandleTrailingSeparator(); + } + + if (mError) { + QM_WARNING("Origin '%s' failed to parse, handled tokens: %s", mOrigin.get(), + mHandledTokens.get()); + + return (mSchemeType == eChrome || mSchemeType == eAbout) ? ObsoleteOrigin + : InvalidOrigin; + } + + MOZ_ASSERT(mState == eComplete || mState == eHandledTrailingSeparator); + + // For IPv6 URL, it should at least have three groups. + MOZ_ASSERT_IF(mIPGroup > 0, mIPGroup >= 3); + + nsAutoCString spec(mScheme); + + if (mSchemeType == eFile) { + spec.AppendLiteral("://"); + + if (mUniversalFileOrigin) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + spec.Append(mPathnameComponents[0]); + } else { + for (uint32_t count = mPathnameComponents.Length(), index = 0; + index < count; index++) { + spec.Append('/'); + spec.Append(mPathnameComponents[index]); + } + } + + aSpec = spec; + + return ValidOrigin; + } + + if (mSchemeType == eAbout) { + if (mMaybeObsolete) { + // The "moz-safe-about+++home" was acciedntally created by a buggy nightly + // and can be safely removed. + return mHost.EqualsLiteral("home") ? ObsoleteOrigin : InvalidOrigin; + } + spec.Append(':'); + } else if (mSchemeType != eChrome) { + spec.AppendLiteral("://"); + } + + spec.Append(mHost); + + if (!mPort.IsNull()) { + spec.Append(':'); + spec.AppendInt(mPort.Value()); + } + + aSpec = spec; + + return mScheme.EqualsLiteral("app") ? ObsoleteOrigin : ValidOrigin; +} + +void OriginParser::HandleScheme(const nsDependentCSubstring& aToken) { + MOZ_ASSERT(!aToken.IsEmpty()); + MOZ_ASSERT(mState == eExpectingAppIdOrScheme || mState == eExpectingScheme); + + bool isAbout = false; + bool isMozSafeAbout = false; + bool isFile = false; + bool isChrome = false; + if (aToken.EqualsLiteral("http") || aToken.EqualsLiteral("https") || + (isAbout = aToken.EqualsLiteral("about") || + (isMozSafeAbout = aToken.EqualsLiteral("moz-safe-about"))) || + aToken.EqualsLiteral("indexeddb") || + (isFile = aToken.EqualsLiteral("file")) || aToken.EqualsLiteral("app") || + aToken.EqualsLiteral("resource") || + aToken.EqualsLiteral("moz-extension") || + (isChrome = aToken.EqualsLiteral(kChromeOrigin)) || + aToken.EqualsLiteral("uuid")) { + mScheme = aToken; + + if (isAbout) { + mSchemeType = eAbout; + mState = isMozSafeAbout ? eExpectingEmptyToken1OrHost : eExpectingHost; + } else if (isChrome) { + mSchemeType = eChrome; + if (mTokenizer.hasMoreTokens()) { + mError = true; + } + mState = eComplete; + } else { + if (isFile) { + mSchemeType = eFile; + } + mState = eExpectingEmptyToken1; + } + + return; + } + + QM_WARNING("'%s' is not a valid scheme!", nsCString(aToken).get()); + + mError = true; +} + +void OriginParser::HandlePathnameComponent( + const nsDependentCSubstring& aToken) { + MOZ_ASSERT(!aToken.IsEmpty()); + MOZ_ASSERT(mState == eExpectingEmptyTokenOrDriveLetterOrPathnameComponent || + mState == eExpectingEmptyTokenOrPathnameComponent); + MOZ_ASSERT(mSchemeType == eFile); + + mPathnameComponents.AppendElement(aToken); + + mState = mTokenizer.hasMoreTokens() ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; +} + +void OriginParser::HandleToken(const nsDependentCSubstring& aToken) { + switch (mState) { + case eExpectingAppIdOrScheme: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected an app id or scheme (not an empty string)!"); + + mError = true; + return; + } + + if (IsAsciiDigit(aToken.First())) { + // nsDependentCSubstring doesn't provice ToInteger() + nsCString token(aToken); + + nsresult rv; + Unused << token.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + mState = eExpectingInMozBrowser; + return; + } + } + + HandleScheme(aToken); + + return; + } + + case eExpectingInMozBrowser: { + if (aToken.Length() != 1) { + QM_WARNING("'%zu' is not a valid length for the inMozBrowser flag!", + aToken.Length()); + + mError = true; + return; + } + + if (aToken.First() == 't') { + mInIsolatedMozBrowser = true; + } else if (aToken.First() == 'f') { + mInIsolatedMozBrowser = false; + } else { + QM_WARNING("'%s' is not a valid value for the inMozBrowser flag!", + nsCString(aToken).get()); + + mError = true; + return; + } + + mState = eExpectingScheme; + + return; + } + + case eExpectingScheme: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected a scheme (not an empty string)!"); + + mError = true; + return; + } + + HandleScheme(aToken); + + return; + } + + case eExpectingEmptyToken1: { + if (!aToken.IsEmpty()) { + QM_WARNING("Expected the first empty token!"); + + mError = true; + return; + } + + mState = eExpectingEmptyToken2; + + return; + } + + case eExpectingEmptyToken2: { + if (!aToken.IsEmpty()) { + QM_WARNING("Expected the second empty token!"); + + mError = true; + return; + } + + if (mSchemeType == eFile) { + mState = eExpectingEmptyTokenOrUniversalFileOrigin; + } else { + if (mSchemeType == eAbout) { + mMaybeObsolete = true; + } + mState = eExpectingHost; + } + + return; + } + + case eExpectingEmptyTokenOrUniversalFileOrigin: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent + : eComplete; + + return; + } + + if (aToken.EqualsLiteral("UNIVERSAL_FILE_URI_ORIGIN")) { + mUniversalFileOrigin = true; + + mPathnameComponents.AppendElement(aToken); + + mState = eComplete; + + return; + } + + QM_WARNING( + "Expected the third empty token or " + "UNIVERSAL_FILE_URI_ORIGIN!"); + + mError = true; + return; + } + + case eExpectingHost: { + if (aToken.IsEmpty()) { + QM_WARNING("Expected a host (not an empty string)!"); + + mError = true; + return; + } + + mHost = aToken; + + if (aToken.First() == '[') { + MOZ_ASSERT(mIPGroup == 0); + + ++mIPGroup; + mState = eExpectingIPV6Token; + + MOZ_ASSERT(mTokenizer.hasMoreTokens()); + return; + } + + if (mTokenizer.hasMoreTokens()) { + if (mSchemeType == eAbout) { + QM_WARNING("Expected an empty string after host!"); + + mError = true; + return; + } + + mState = eExpectingPort; + + return; + } + + mState = eComplete; + + return; + } + + case eExpectingPort: { + MOZ_ASSERT(mSchemeType == eNone); + + if (aToken.IsEmpty()) { + QM_WARNING("Expected a port (not an empty string)!"); + + mError = true; + return; + } + + // nsDependentCSubstring doesn't provice ToInteger() + nsCString token(aToken); + + nsresult rv; + uint32_t port = token.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) { + mPort.SetValue() = port; + } else { + QM_WARNING("'%s' is not a valid port number!", token.get()); + + mError = true; + return; + } + + mState = eComplete; + + return; + } + + case eExpectingEmptyTokenOrDriveLetterOrPathnameComponent: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + mPathnameComponents.AppendElement(""_ns); + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + if (aToken.Length() == 1 && IsAsciiAlpha(aToken.First())) { + mMaybeDriveLetter = true; + + mPathnameComponents.AppendElement(aToken); + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + HandlePathnameComponent(aToken); + + return; + } + + case eExpectingEmptyTokenOrPathnameComponent: { + MOZ_ASSERT(mSchemeType == eFile); + + if (aToken.IsEmpty()) { + if (mMaybeDriveLetter) { + MOZ_ASSERT(mPathnameComponents.Length() == 1); + + nsCString& pathnameComponent = mPathnameComponents[0]; + pathnameComponent.Append(':'); + + mMaybeDriveLetter = false; + } else { + mPathnameComponents.AppendElement(""_ns); + } + + mState = mTokenizer.hasMoreTokens() + ? eExpectingEmptyTokenOrPathnameComponent + : eComplete; + + return; + } + + HandlePathnameComponent(aToken); + + return; + } + + case eExpectingEmptyToken1OrHost: { + MOZ_ASSERT(mSchemeType == eAbout && + mScheme.EqualsLiteral("moz-safe-about")); + + if (aToken.IsEmpty()) { + mState = eExpectingEmptyToken2; + } else { + mHost = aToken; + mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete; + } + + return; + } + + case eExpectingIPV6Token: { + // A safe check for preventing infinity recursion. + if (++mIPGroup > 8) { + mError = true; + return; + } + + mHost.AppendLiteral(":"); + mHost.Append(aToken); + if (!aToken.IsEmpty() && aToken.Last() == ']') { + mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete; + } + + return; + } + + default: + MOZ_CRASH("Should never get here!"); + } +} + +void OriginParser::HandleTrailingSeparator() { + MOZ_ASSERT(mState == eComplete); + MOZ_ASSERT(mSchemeType == eFile); + + mPathnameComponents.AppendElement(""_ns); + + mState = eHandledTrailingSeparator; +} + +nsresult RepositoryOperationBase::ProcessRepository() { + AssertIsOnIOThread(); + +#ifdef DEBUG + { + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(mDirectory, Exists), + QM_ASSERT_UNREACHABLE); + MOZ_ASSERT(exists); + } +#endif + + QM_TRY(CollectEachFileEntry( + *mDirectory, + [](const auto& originFile) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsAutoString, originFile, GetLeafName)); + + // Unknown files during upgrade are allowed. Just warn if we find + // them. + if (!IsOSMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + return mozilla::Ok{}; + }, + [&self = *this](const auto& originDir) -> Result<mozilla::Ok, nsresult> { + OriginProps originProps(WrapMovingNotNullUnchecked(originDir)); + QM_TRY(MOZ_TO_RESULT(originProps.Init([&self](const auto& aSpec) { + return self.PersistenceTypeFromSpec(aSpec); + }))); + // Bypass invalid origins while upgrading + QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), mozilla::Ok{}); + + if (originProps.mType != OriginProps::eObsolete) { + QM_TRY_INSPECT(const bool& removed, + MOZ_TO_RESULT_INVOKE_MEMBER( + self, PrepareOriginDirectory, originProps)); + if (removed) { + return mozilla::Ok{}; + } + } + + self.mOriginProps.AppendElement(std::move(originProps)); + + return mozilla::Ok{}; + })); + + if (mOriginProps.IsEmpty()) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories())); + + return NS_OK; +} + +template <typename UpgradeMethod> +nsresult RepositoryOperationBase::MaybeUpgradeClients( + const OriginProps& aOriginProps, UpgradeMethod aMethod) { + AssertIsOnIOThread(); + MOZ_ASSERT(aMethod); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY(CollectEachFileEntry( + *aOriginProps.mDirectory, + [](const auto& file) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName)); + + if (!IsOriginMetadata(leafName) && !IsTempMetadata(leafName)) { + UNKNOWN_FILE_WARNING(leafName); + } + + return mozilla::Ok{}; + }, + [quotaManager, &aMethod, + &self = *this](const auto& dir) -> Result<mozilla::Ok, nsresult> { + QM_TRY_INSPECT( + const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, dir, GetLeafName)); + + QM_TRY_INSPECT(const bool& removed, + MOZ_TO_RESULT_INVOKE_MEMBER(self, PrepareClientDirectory, + dir, leafName)); + if (removed) { + return mozilla::Ok{}; + } + + Client::Type clientType; + bool ok = Client::TypeFromText(leafName, clientType, fallible); + if (!ok) { + UNKNOWN_FILE_WARNING(leafName); + return mozilla::Ok{}; + } + + Client* client = quotaManager->GetClient(clientType); + MOZ_ASSERT(client); + + QM_TRY(MOZ_TO_RESULT((client->*aMethod)(dir))); + + return mozilla::Ok{}; + })); + + return NS_OK; +} + +nsresult RepositoryOperationBase::PrepareClientDirectory( + nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) { + AssertIsOnIOThread(); + + aRemoved = false; + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::Init() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDirectory); + + const auto maybeLegacyPersistenceType = + LegacyPersistenceTypeFromFile(*mDirectory, fallible); + QM_TRY(OkIf(maybeLegacyPersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + mLegacyPersistenceType.init(maybeLegacyPersistenceType.value()); + + return NS_OK; +} + +Maybe<CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceType> +CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceTypeFromFile( + nsIFile& aFile, const fallible_t&) { + nsAutoString leafName; + MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName)); + + if (leafName.Equals(u"persistent"_ns)) { + return Some(LegacyPersistenceType::Persistent); + } + + if (leafName.Equals(u"temporary"_ns)) { + return Some(LegacyPersistenceType::Temporary); + } + + return Nothing(); +} + +PersistenceType +CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromLegacyPersistentSpec( + const nsCString& aSpec) { + if (QuotaManager::IsOriginInternal(aSpec)) { + return PERSISTENCE_TYPE_PERSISTENT; + } + + return PERSISTENCE_TYPE_DEFAULT; +} + +PersistenceType CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromSpec( + const nsCString& aSpec) { + switch (*mLegacyPersistenceType) { + case LegacyPersistenceType::Persistent: + return PersistenceTypeFromLegacyPersistentSpec(aSpec); + case LegacyPersistenceType::Temporary: + return PERSISTENCE_TYPE_TEMPORARY; + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad legacy persistence type value!"); +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::MaybeUpgradeOriginDirectory( + nsIFile* aDirectory) { + AssertIsOnIOThread(); + MOZ_ASSERT(aDirectory); + + QM_TRY_INSPECT( + const auto& metadataFile, + CloneFileAndAppend(*aDirectory, nsLiteralString(METADATA_FILE_NAME))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(metadataFile, Exists)); + + if (!exists) { + // Directory structure upgrade needed. + // Move all files to IDB specific directory. + + nsString idbDirectoryName; + QM_TRY(OkIf(Client::TypeToText(Client::IDB, idbDirectoryName, fallible)), + NS_ERROR_FAILURE); + + QM_TRY_INSPECT(const auto& idbDirectory, + CloneFileAndAppend(*aDirectory, idbDirectoryName)); + + // Usually we only use QM_OR_ELSE_LOG_VERBOSE/QM_OR_ELSE_LOG_VERBOSE_IF + // with Create and NS_ERROR_FILE_ALREADY_EXISTS check, but typically the + // idb directory shouldn't exist during the upgrade and the upgrade runs + // only once in most of the cases, so the use of QM_OR_ELSE_WARN_IF is ok + // here. + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT(idbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)), + // Predicate. + IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>, + // Fallback. + ([&idbDirectory](const nsresult rv) -> Result<Ok, nsresult> { + QM_TRY_INSPECT( + const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(idbDirectory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED)); + + return Ok{}; + }))); + + QM_TRY(CollectEachFile( + *aDirectory, + [&idbDirectory, &idbDirectoryName]( + const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> { + QM_TRY_INSPECT(const auto& leafName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, + GetLeafName)); + + if (!leafName.Equals(idbDirectoryName)) { + QM_TRY(MOZ_TO_RESULT(file->MoveTo(idbDirectory, u""_ns))); + } + + return Ok{}; + })); + + QM_TRY( + MOZ_TO_RESULT(metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + } + + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) { + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeOriginDirectory(aOriginProps.mDirectory.get()))); + + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + } else { + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore = true; + } else if (!isApp.IsNull()) { + aOriginProps.mIgnore = true; + } + } + + *aRemoved = false; + return NS_OK; +} + +nsresult CreateOrUpgradeDirectoryMetadataHelper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + + // Move internal origins to new persistent storage. + if (PersistenceTypeFromLegacyPersistentSpec(aOriginProps.mSpec) == + PERSISTENCE_TYPE_PERSISTENT) { + if (!mPermanentStorageDir) { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + const nsString& permanentStoragePath = + quotaManager->GetStoragePath(PERSISTENCE_TYPE_PERSISTENT); + + QM_TRY_UNWRAP(mPermanentStorageDir, + QM_NewLocalFile(permanentStoragePath)); + } + + const nsAString& leafName = aOriginProps.mLeafName; + + QM_TRY_INSPECT(const auto& newDirectory, + CloneFileAndAppend(*mPermanentStorageDir, leafName)); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(newDirectory, Exists)); + + if (exists) { + QM_WARNING("Found %s in storage/persistent and storage/permanent !", + NS_ConvertUTF16toUTF8(leafName).get()); + + QM_TRY(MOZ_TO_RESULT( + aOriginProps.mDirectory->Remove(/* recursive */ true))); + } else { + QM_TRY(MOZ_TO_RESULT( + aOriginProps.mDirectory->MoveTo(mPermanentStorageDir, u""_ns))); + } + } + } else if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } else if (!aOriginProps.mIgnore) { + QM_TRY_INSPECT(const auto& file, + CloneFileAndAppend(*aOriginProps.mDirectory, + nsLiteralString(METADATA_FILE_NAME))); + + QM_TRY_INSPECT(const auto& stream, + GetBinaryOutputStream(*file, FileFlag::Append)); + + MOZ_ASSERT(stream); + + // Currently unused (used to be isApp). + QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false))); + } + + return NS_OK; +} + +nsresult UpgradeStorageHelperBase::Init() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDirectory); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*mDirectory, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + mPersistenceType.init(maybePersistenceType.value()); + + return NS_OK; +} + +PersistenceType UpgradeStorageHelperBase::PersistenceTypeFromSpec( + const nsCString& aSpec) { + // There's no moving of origin directories between repositories like in the + // CreateOrUpgradeDirectoryMetadataHelper + return *mPersistenceType; +} + +nsresult UpgradeStorageFrom0_0To1_0Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom0_0To1_0Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // This handles changes in origin string generation from nsIPrincipal, + // especially the change from: appId+inMozBrowser+originNoSuffix + // to: origin (with origin suffix). + QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps)); + if (renamed) { + return NS_OK; + } + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + return NS_OK; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveMorgueDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // The Cache API was creating top level morgue directories by accident for + // a short time in nightly. This unfortunately prevents all storage from + // working. So recover these profiles permanently by removing these corrupt + // directories as part of this upgrade. + + QM_TRY_INSPECT(const auto& morgueDir, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, Clone)); + + QM_TRY(MOZ_TO_RESULT(morgueDir->Append(u"morgue"_ns))); + + QM_TRY_INSPECT(const bool& exists, + MOZ_TO_RESULT_INVOKE_MEMBER(morgueDir, Exists)); + + if (exists) { + QM_WARNING("Deleting accidental morgue directory!"); + + QM_TRY(MOZ_TO_RESULT(morgueDir->Remove(/* recursive */ true))); + } + + return NS_OK; +} + +Result<bool, nsresult> UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveAppsData( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // TODO: This method was empty for some time due to accidental changes done + // in bug 1320404. This led to renaming of origin directories like: + // https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1 + // to: + // https+++developer.cdn.mozilla.net^inBrowser=1 + // instead of just removing them. + + const nsCString& originalSuffix = aOriginProps.mOriginalSuffix; + if (!originalSuffix.IsEmpty()) { + MOZ_ASSERT(originalSuffix[0] == '^'); + + if (!URLParams::Parse( + Substring(originalSuffix, 1, originalSuffix.Length() - 1), + [](const nsAString& aName, const nsAString& aValue) { + if (aName.EqualsLiteral("appId")) { + return false; + } + + return true; + })) { + QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(aOriginProps))); + + return true; + } + } + + return false; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + QM_TRY(MOZ_TO_RESULT(MaybeRemoveMorgueDirectory(aOriginProps))); + + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom1_0To2_0))); + + QM_TRY_INSPECT(const bool& removed, MaybeRemoveAppsData(aOriginProps)); + if (removed) { + *aRemoved = true; + return NS_OK; + } + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2, + MOZ_TO_RESULT(GetDirectoryMetadata2( + aOriginProps.mDirectory.get(), timestamp, suffix, + group, origin, isApp.SetValue()))); + if (!maybeDirectoryMetadata2) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom1_0To2_0Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // This handles changes in origin string generation from nsIPrincipal, + // especially the stripping of obsolete origin attributes like addonId. + QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps)); + if (renamed) { + return NS_OK; + } + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_0To2_1Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_0To2_1))); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2, + MOZ_TO_RESULT(GetDirectoryMetadata2( + aOriginProps.mDirectory.get(), timestamp, suffix, + group, origin, isApp.SetValue()))); + if (!maybeDirectoryMetadata2) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom2_0To2_1Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareOriginDirectory( + OriginProps& aOriginProps, bool* aRemoved) { + AssertIsOnIOThread(); + MOZ_ASSERT(aRemoved); + + QM_TRY(MOZ_TO_RESULT( + MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_1To2_2))); + + int64_t timestamp; + nsCString group; + nsCString origin; + Nullable<bool> isApp; + QM_WARNONLY_TRY_UNWRAP( + const auto maybeDirectoryMetadata, + MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(), + timestamp, group, origin, isApp))); + if (!maybeDirectoryMetadata || isApp.IsNull()) { + aOriginProps.mNeedsRestore = true; + } + + nsCString suffix; + QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2, + MOZ_TO_RESULT(GetDirectoryMetadata2( + aOriginProps.mDirectory.get(), timestamp, suffix, + group, origin, isApp.SetValue()))); + if (!maybeDirectoryMetadata2) { + aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps); + aOriginProps.mNeedsRestore2 = true; + } else { + aOriginProps.mTimestamp = timestamp; + } + + *aRemoved = false; + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + if (aOriginProps.mNeedsRestore) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + } + + return NS_OK; +} + +nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareClientDirectory( + nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) { + AssertIsOnIOThread(); + + if (Client::IsDeprecatedClient(aLeafName)) { + QM_WARNING("Deleting deprecated %s client!", + NS_ConvertUTF16toUTF8(aLeafName).get()); + + QM_TRY(MOZ_TO_RESULT(aFile->Remove(true))); + + aRemoved = true; + } else { + aRemoved = false; + } + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::Init() { + AssertIsOnIOThread(); + MOZ_ASSERT(mDirectory); + + nsCOMPtr<nsIFile> parentDir; + QM_TRY(MOZ_TO_RESULT(mDirectory->GetParent(getter_AddRefs(parentDir)))); + + const auto maybePersistenceType = + PersistenceTypeFromFile(*parentDir, fallible); + QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE)); + + mPersistenceType.init(maybePersistenceType.value()); + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::RestoreMetadata2File() { + OriginProps originProps(WrapMovingNotNull(mDirectory)); + QM_TRY(MOZ_TO_RESULT(originProps.Init( + [&self = *this](const auto& aSpec) { return *self.mPersistenceType; }))); + + QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), NS_ERROR_FAILURE); + + originProps.mTimestamp = GetOriginLastModifiedTime(originProps); + + mOriginProps.AppendElement(std::move(originProps)); + + QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories())); + + return NS_OK; +} + +nsresult RestoreDirectoryMetadata2Helper::ProcessOriginDirectory( + const OriginProps& aOriginProps) { + AssertIsOnIOThread(); + + // We don't have any approach to restore aPersisted, so reset it to false. + QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + return NS_OK; +} + +} // namespace mozilla::dom::quota |