diff options
Diffstat (limited to 'dom/quota/ActorsParent.cpp')
-rw-r--r-- | dom/quota/ActorsParent.cpp | 7874 |
1 files changed, 7874 insertions, 0 deletions
diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp new file mode 100644 index 0000000000..3f8a1a316b --- /dev/null +++ b/dom/quota/ActorsParent.cpp @@ -0,0 +1,7874 @@ +/* -*- 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 "NormalOriginOperationBase.h" +#include "OriginOperationBase.h" +#include "OriginOperations.h" +#include "OriginParser.h" +#include "OriginScope.h" +#include "OriginInfo.h" +#include "QuotaCommon.h" +#include "QuotaManager.h" +#include "ResolvableNormalOriginOp.h" +#include "SanitizationUtils.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/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/FileSystemQuotaClientFactory.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/Config.h" +#include "mozilla/dom/quota/Constants.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/FileUtils.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaManagerService.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/ScopedLogExtraInfo.h" +#include "mozilla/dom/quota/StreamUtils.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 "nsClassHashtable.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsDirectoryServiceUtils.h" +#include "nsError.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 "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 kAboutHomeOriginPrefix[] = "moz-safe-about:home"; +const char kIndexedDBOriginPrefix[] = "indexeddb://"; +const char kResourceOriginPrefix[] = "resource://"; + +constexpr auto kStorageName = u"storage"_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 PRIVATE_DIRECTORY_NAME u"private" +#define TOBEREMOVED_DIRECTORY_NAME u"to-be-removed" + +#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; +}; + +/******************************************************************************* + * 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 RecordTimeDeltaHelper final : public Runnable { + const Telemetry::HistogramID mHistogram; + + // 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: + explicit RecordTimeDeltaHelper(const Telemetry::HistogramID aHistogram) + : Runnable("dom::quota::RecordTimeDeltaHelper"), mHistogram(aHistogram) {} + + TimeStamp Start(); + + TimeStamp End(); + + private: + ~RecordTimeDeltaHelper() = default; + + NS_DECL_NSIRUNNABLE +}; + +/******************************************************************************* + * Helper classes + ******************************************************************************/ + +/******************************************************************************* + * Helper Functions + ******************************************************************************/ + +// 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; +} + +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; + +// XXX Move to QuotaManager once NormalOriginOperationBase is declared in a +// separate and includable file. +using NormalOriginOpArray = + nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>; +StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps; + +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 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; +}; + +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; +} + +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('+'); +} + +// 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); + + 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 +} + +void InitializeScopedLogExtraInfo() { +#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED + ScopedLogExtraInfo::Initialize(); +#endif +} + +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), + mShutdownStorageOpCount(0), + mStorageInitialized(false), + mTemporaryStorageInitialized(false), + mTemporaryStorageInitializedInternal(false), + mCacheUsable(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) { + auto recordTimeDeltaHelper = + MakeRefPtr<RecordTimeDeltaHelper>(Telemetry::QM_SHUTDOWN_TIME_V0); + + recordTimeDeltaHelper->Start(); + + gInstance->Shutdown(); + + recordTimeDeltaHelper->End(); + + 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 mozilla::dom::quota::IsOSMetadata(aFileName); +} + +// static +bool QuotaManager::IsDotFile(const nsAString& aFileName) { + return mozilla::dom::quota::IsDotFile(aFileName); +} + +void QuotaManager::RegisterNormalOriginOp( + NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + + if (!gNormalOriginOps) { + gNormalOriginOps = new NormalOriginOpArray(); + } + + gNormalOriginOps->AppendElement(&aNormalOriginOp); +} + +void QuotaManager::UnregisterNormalOriginOp( + NormalOriginOperationBase& aNormalOriginOp) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(gNormalOriginOps); + + gNormalOriginOps->RemoveElement(&aNormalOriginOp); + + if (gNormalOriginOps->IsEmpty()) { + gNormalOriginOps = nullptr; + } +} + +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()); + + // ClearDirectoryLockTables may have been called, so the element or entire + // array may not exist anymre. + nsTArray<NotNull<DirectoryLockImpl*>>* array; + if (directoryLockTable.Get(aLock.Origin(), &array) && + array->RemoveElement(&aLock) && 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; +} + +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(PRIVATE_DIRECTORY_NAME))); + + QM_TRY_UNWRAP( + do_Init(mToBeRemovedStoragePath), + GetPathForStorage(*baseDir, nsLiteralString(TOBEREMOVED_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 QM_COLLECTING_OPERATION_TELEMETRY + for (const auto& op : *gNormalOriginOps) { + annotation.AppendPrintf("Op: %s pending\n", op->Name()); + } +#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 = CreateSaveOriginAccessTimeOp(WrapMovingNotNullUnchecked(this), + 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(!mTemporaryStorageInitializedInternal); + + // 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 recordTimeDeltaHelper = + MakeRefPtr<RecordTimeDeltaHelper>(Telemetry::QM_QUOTA_INFO_LOAD_TIME_V0); + + const auto startTime = recordTimeDeltaHelper->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_FILE_NOT_FOUND)); + + QM_TRY_INSPECT(const bool& isDirectory, + MOZ_TO_RESULT_INVOKE_MEMBER(directory, IsDirectory)); + + QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FILE_DESTINATION_NOT_DIR)); + + // 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_WARNONLY_TRY(OkIf(fullOriginMetadata.mLastAccessTime == + metadata.mLastAccessTime)); + + 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 = recordTimeDeltaHelper->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(mTemporaryStorageInitializedInternal); + 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_ASSERT(aDirectoryLockId == -1); + return nullptr; +} + +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; +} + +// static +nsresult QuotaManager::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; +} + +// static +nsresult QuotaManager::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; +} + +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)); + + // XXX Consider using QuotaManager::ParseOrigin here. + 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()}; +} + +Result<Ok, nsresult> QuotaManager::RemoveOriginDirectory(nsIFile& aDirectory) { + AssertIsOnIOThread(); + + if (!AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownTeardown)) { + QM_TRY_RETURN(MOZ_TO_RESULT(aDirectory.Remove(true))); + } + + QM_TRY_INSPECT(const auto& toBeRemovedStorageDir, + QM_NewLocalFile(*mToBeRemovedStoragePath)); + + QM_TRY_INSPECT(const bool& created, EnsureDirectory(*toBeRemovedStorageDir)); + + (void)created; + + QM_TRY_RETURN(MOZ_TO_RESULT(aDirectory.MoveTo( + toBeRemovedStorageDir, NSID_TrimBracketsUTF16(nsID::GenerateUUID())))); +} + +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) + // EnsureStorageIsInitializedInternal 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::AssertStorageIsInitializedInternal() const { + AssertIsOnIOThread(); + MOZ_ASSERT(IsStorageInitializedInternal()); +} + +#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{}; +} + +RefPtr<BoolPromise> QuotaManager::InitializeStorage() { + AssertIsOnOwningThread(); + + // If storage is initialized but there's a clear storage or shutdown storage + // operation already scheduled, we can't immediately resolve the promise and + // return from the function because the clear or shutdown storage operation + // uninitializes storage. + if (mStorageInitialized && !mShutdownStorageOpCount) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + RefPtr<UniversalDirectoryLock> directoryLock = CreateDirectoryLockInternal( + Nullable<PersistenceType>(), OriginScope::FromNull(), + Nullable<Client::Type>(), + /* aExclusive */ false); + + return directoryLock->Acquire()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this), + directoryLock](const BoolPromise::ResolveOrRejectValue& aValue) mutable { + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + return self->InitializeStorage(std::move(directoryLock)); + }); +} + +RefPtr<BoolPromise> QuotaManager::InitializeStorage( + RefPtr<UniversalDirectoryLock> aDirectoryLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aDirectoryLock); + + if (mStorageInitialized && !mShutdownStorageOpCount) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + auto initializeStorageOp = + CreateInitOp(WrapMovingNotNullUnchecked(this), std::move(aDirectoryLock)); + + RegisterNormalOriginOp(*initializeStorageOp); + + initializeStorageOp->RunImmediately(); + + return initializeStorageOp->OnResults()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + self->mStorageInitialized = true; + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +RefPtr<BoolPromise> QuotaManager::StorageInitialized() { + AssertIsOnOwningThread(); + + auto storageInitializedOp = + CreateStorageInitializedOp(WrapMovingNotNullUnchecked(this)); + + RegisterNormalOriginOp(*storageInitializedOp); + + storageInitializedOp->RunImmediately(); + + return storageInitializedOp->OnResults(); +} + +nsresult QuotaManager::EnsureStorageIsInitializedInternal() { + 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<BoolPromise> QuotaManager::TemporaryStorageInitialized() { + AssertIsOnOwningThread(); + + auto temporaryStorageInitializedOp = + CreateTemporaryStorageInitializedOp(WrapMovingNotNullUnchecked(this)); + + RegisterNormalOriginOp(*temporaryStorageInitializedOp); + + temporaryStorageInitializedOp->RunImmediately(); + + return temporaryStorageInitializedOp->OnResults(); +} + +RefPtr<UniversalDirectoryLockPromise> QuotaManager::OpenStorageDirectory( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + bool aExclusive, DirectoryLockCategory aCategory, + Maybe<RefPtr<UniversalDirectoryLock>&> aPendingDirectoryLockOut) { + AssertIsOnOwningThread(); + + RefPtr<UniversalDirectoryLock> storageDirectoryLock; + + RefPtr<BoolPromise> storageDirectoryLockPromise; + + if (mStorageInitialized && !mShutdownStorageOpCount) { + storageDirectoryLockPromise = BoolPromise::CreateAndResolve(true, __func__); + } else { + storageDirectoryLock = CreateDirectoryLockInternal( + Nullable<PersistenceType>(), OriginScope::FromNull(), + Nullable<Client::Type>(), + /* aExclusive */ false); + + storageDirectoryLockPromise = storageDirectoryLock->Acquire(); + } + + RefPtr<UniversalDirectoryLock> universalDirectoryLock = + CreateDirectoryLockInternal(aPersistenceType, aOriginScope, aClientType, + aExclusive, aCategory); + + RefPtr<BoolPromise> universalDirectoryLockPromise = + universalDirectoryLock->Acquire(); + + if (aPendingDirectoryLockOut.isSome()) { + aPendingDirectoryLockOut.ref() = universalDirectoryLock; + } + + return storageDirectoryLockPromise + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this), + storageDirectoryLock = std::move(storageDirectoryLock)]( + const BoolPromise::ResolveOrRejectValue& aValue) mutable { + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), + __func__); + } + + if (!storageDirectoryLock) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + return self->InitializeStorage(std::move(storageDirectoryLock)); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [universalDirectoryLockPromise = + std::move(universalDirectoryLockPromise)]( + const BoolPromise::ResolveOrRejectValue& aValue) mutable { + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), + __func__); + } + + return std::move(universalDirectoryLockPromise); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [universalDirectoryLock = std::move(universalDirectoryLock)]( + const BoolPromise::ResolveOrRejectValue& aValue) mutable { + if (aValue.IsReject()) { + return UniversalDirectoryLockPromise::CreateAndReject( + aValue.RejectValue(), __func__); + } + + return UniversalDirectoryLockPromise::CreateAndResolve( + std::move(universalDirectoryLock), __func__); + }); +} + +RefPtr<ClientDirectoryLockPromise> QuotaManager::OpenClientDirectory( + const ClientMetadata& aClientMetadata, + Maybe<RefPtr<ClientDirectoryLock>&> aPendingDirectoryLockOut) { + AssertIsOnOwningThread(); + + nsTArray<RefPtr<BoolPromise>> promises; + + RefPtr<UniversalDirectoryLock> storageDirectoryLock; + + if (!mStorageInitialized || mShutdownStorageOpCount) { + storageDirectoryLock = CreateDirectoryLockInternal( + Nullable<PersistenceType>(), OriginScope::FromNull(), + Nullable<Client::Type>(), + /* aExclusive */ false); + promises.AppendElement(storageDirectoryLock->Acquire()); + } + + RefPtr<ClientDirectoryLock> clientDirectoryLock = + CreateDirectoryLock(aClientMetadata, /* aExclusive */ false); + + promises.AppendElement(clientDirectoryLock->Acquire()); + + if (aPendingDirectoryLockOut.isSome()) { + aPendingDirectoryLockOut.ref() = clientDirectoryLock; + } + + return BoolPromise::All(GetCurrentSerialEventTarget(), promises) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this), + storageDirectoryLock = std::move(storageDirectoryLock)]( + const CopyableTArray<bool>& aResolveValues) mutable { + if (!storageDirectoryLock) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + return self->InitializeStorage(std::move(storageDirectoryLock)); + }, + [](nsresult aRejectValue) { + return BoolPromise::CreateAndReject(aRejectValue, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [clientDirectoryLock = std::move(clientDirectoryLock)]( + const BoolPromise::ResolveOrRejectValue& aValue) mutable { + if (aValue.IsReject()) { + return ClientDirectoryLockPromise::CreateAndReject( + aValue.RejectValue(), __func__); + } + return ClientDirectoryLockPromise::CreateAndResolve( + std::move(clientDirectoryLock), __func__); + }); +} + +RefPtr<ClientDirectoryLock> QuotaManager::CreateDirectoryLock( + const ClientMetadata& aClientMetadata, bool aExclusive) { + AssertIsOnOwningThread(); + + return DirectoryLockImpl::Create( + WrapNotNullUnchecked(this), aClientMetadata.mPersistenceType, + aClientMetadata, aClientMetadata.mClientType, aExclusive); +} + +RefPtr<UniversalDirectoryLock> QuotaManager::CreateDirectoryLockInternal( + const Nullable<PersistenceType>& aPersistenceType, + const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType, + bool aExclusive, DirectoryLockCategory aCategory) { + AssertIsOnOwningThread(); + + return DirectoryLockImpl::CreateInternal(WrapNotNullUnchecked(this), + aPersistenceType, aOriginScope, + aClientType, aExclusive, aCategory); +} + +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); +} + +bool QuotaManager::IsTemporaryOriginInitialized( + const OriginMetadata& aOriginMetadata) const { + AssertIsOnIOThread(); + MOZ_ASSERT(aOriginMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + + MutexAutoLock lock(mQuotaMutex); + + RefPtr<OriginInfo> originInfo = + LockedGetOriginInfo(aOriginMetadata.mPersistenceType, aOriginMetadata); + + return static_cast<bool>(originInfo); +} + +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(mTemporaryStorageInitializedInternal); + + 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); +} + +RefPtr<BoolPromise> QuotaManager::InitializePersistentClient( + const PrincipalInfo& aPrincipalInfo, Client::Type aClientType) { + AssertIsOnOwningThread(); + + auto initializePersistentClientOp = CreateInitializePersistentClientOp( + WrapMovingNotNullUnchecked(this), aPrincipalInfo, aClientType); + + RegisterNormalOriginOp(*initializePersistentClientOp); + + initializePersistentClientOp->RunImmediately(); + + return initializePersistentClientOp->OnResults(); +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsurePersistentClientIsInitialized( + const ClientMetadata& aClientMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aClientMetadata.mPersistenceType == PERSISTENCE_TYPE_PERSISTENT); + MOZ_ASSERT(Client::IsValidType(aClientMetadata.mClientType)); + MOZ_DIAGNOSTIC_ASSERT(IsStorageInitializedInternal()); + MOZ_DIAGNOSTIC_ASSERT(IsOriginInitialized(aClientMetadata.mOrigin)); + + QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aClientMetadata)); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(Client::TypeToString(aClientMetadata.mClientType)))); + + QM_TRY_UNWRAP(bool created, EnsureDirectory(*directory)); + + return std::pair(std::move(directory), created); +} + +RefPtr<BoolPromise> QuotaManager::InitializeTemporaryClient( + PersistenceType aPersistenceType, const PrincipalInfo& aPrincipalInfo, + Client::Type aClientType) { + AssertIsOnOwningThread(); + + auto initializeTemporaryClientOp = CreateInitializeTemporaryClientOp( + WrapMovingNotNullUnchecked(this), aPersistenceType, aPrincipalInfo, + aClientType); + + RegisterNormalOriginOp(*initializeTemporaryClientOp); + + initializeTemporaryClientOp->RunImmediately(); + + return initializeTemporaryClientOp->OnResults(); +} + +Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> +QuotaManager::EnsureTemporaryClientIsInitialized( + const ClientMetadata& aClientMetadata) { + AssertIsOnIOThread(); + MOZ_ASSERT(aClientMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT); + MOZ_ASSERT(Client::IsValidType(aClientMetadata.mClientType)); + MOZ_DIAGNOSTIC_ASSERT(IsStorageInitializedInternal()); + MOZ_DIAGNOSTIC_ASSERT(IsTemporaryStorageInitializedInternal()); + MOZ_DIAGNOSTIC_ASSERT(IsTemporaryOriginInitialized(aClientMetadata)); + + QM_TRY_UNWRAP(auto directory, GetOriginDirectory(aClientMetadata)); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(Client::TypeToString(aClientMetadata.mClientType)))); + + QM_TRY_UNWRAP(bool created, EnsureDirectory(*directory)); + + return std::pair(std::move(directory), created); +} + +RefPtr<BoolPromise> QuotaManager::InitializeTemporaryStorage() { + AssertIsOnOwningThread(); + + // If temporary storage is initialized but there's a clear storage or + // shutdown storage operation already scheduled, we can't immediately resolve + // the promise and return from the function because the clear or shutdown + // storage operation uninitializes storage. + if (mTemporaryStorageInitialized && !mShutdownStorageOpCount) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + RefPtr<UniversalDirectoryLock> directoryLock = CreateDirectoryLockInternal( + Nullable<PersistenceType>(), OriginScope::FromNull(), + Nullable<Client::Type>(), + /* aExclusive */ false); + + return directoryLock->Acquire()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this), + directoryLock](const BoolPromise::ResolveOrRejectValue& aValue) mutable { + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + return self->InitializeTemporaryStorage(std::move(directoryLock)); + }); +} + +RefPtr<BoolPromise> QuotaManager::InitializeTemporaryStorage( + RefPtr<UniversalDirectoryLock> aDirectoryLock) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aDirectoryLock); + + if (mTemporaryStorageInitialized && !mShutdownStorageOpCount) { + return BoolPromise::CreateAndResolve(true, __func__); + } + + auto initializeTemporaryStorageOp = CreateInitTemporaryStorageOp( + WrapMovingNotNullUnchecked(this), std::move(aDirectoryLock)); + + RegisterNormalOriginOp(*initializeTemporaryStorageOp); + + initializeTemporaryStorageOp->RunImmediately(); + + return initializeTemporaryStorageOp->OnResults()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + self->mTemporaryStorageInitialized = true; + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +nsresult QuotaManager::EnsureTemporaryStorageIsInitializedInternal() { + AssertIsOnIOThread(); + MOZ_DIAGNOSTIC_ASSERT(mStorageConnection); + + const auto innerFunc = + [&](const auto& firstInitializationAttempt) -> nsresult { + if (mTemporaryStorageInitializedInternal) { + 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())); + + mTemporaryStorageInitializedInternal = true; + + CleanupTemporaryStorage(); + + if (mCacheUsable) { + QM_TRY(InvalidateCache(*mStorageConnection)); + } + + return NS_OK; + }; + + return ExecuteInitialization( + Initialization::TemporaryStorage, + "dom::quota::FirstInitializationAttempt::TemporaryStorage"_ns, innerFunc); +} + +RefPtr<BoolPromise> QuotaManager::ClearStoragesForOrigin( + const Maybe<PersistenceType>& aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + const Maybe<Client::Type>& aClientType) { + AssertIsOnOwningThread(); + + auto clearOriginOp = + CreateClearOriginOp(WrapMovingNotNullUnchecked(this), aPersistenceType, + aPrincipalInfo, aClientType); + + RegisterNormalOriginOp(*clearOriginOp); + + clearOriginOp->RunImmediately(); + + return clearOriginOp->OnResults(); +} + +RefPtr<BoolPromise> QuotaManager::ClearStoragesForOriginPrefix( + const Maybe<PersistenceType>& aPersistenceType, + const PrincipalInfo& aPrincipalInfo) { + AssertIsOnOwningThread(); + + auto clearStoragesForOriginPrefixOp = CreateClearStoragesForOriginPrefixOp( + WrapMovingNotNullUnchecked(this), aPersistenceType, aPrincipalInfo); + + RegisterNormalOriginOp(*clearStoragesForOriginPrefixOp); + + clearStoragesForOriginPrefixOp->RunImmediately(); + + return clearStoragesForOriginPrefixOp->OnResults(); +} + +RefPtr<BoolPromise> QuotaManager::ClearStoragesForOriginAttributesPattern( + const OriginAttributesPattern& aPattern) { + AssertIsOnOwningThread(); + + auto clearDataOp = + CreateClearDataOp(WrapMovingNotNullUnchecked(this), aPattern); + + RegisterNormalOriginOp(*clearDataOp); + + clearDataOp->RunImmediately(); + + return clearDataOp->OnResults(); +} + +RefPtr<BoolPromise> QuotaManager::ClearPrivateRepository() { + AssertIsOnOwningThread(); + + auto clearPrivateRepositoryOp = + CreateClearPrivateRepositoryOp(WrapMovingNotNullUnchecked(this)); + + RegisterNormalOriginOp(*clearPrivateRepositoryOp); + + clearPrivateRepositoryOp->RunImmediately(); + + return clearPrivateRepositoryOp->OnResults(); +} + +RefPtr<BoolPromise> QuotaManager::ClearStorage() { + AssertIsOnOwningThread(); + + auto clearStorageOp = CreateClearStorageOp(WrapMovingNotNullUnchecked(this)); + + RegisterNormalOriginOp(*clearStorageOp); + + clearStorageOp->RunImmediately(); + + // Storage clearing also shuts it down, so we need to increses the counter + // here as well. + mShutdownStorageOpCount++; + + return clearStorageOp->OnResults()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue& aValue) { + self->mShutdownStorageOpCount--; + + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + self->mTemporaryStorageInitialized = false; + self->mStorageInitialized = false; + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +RefPtr<BoolPromise> QuotaManager::ShutdownStorage() { + AssertIsOnOwningThread(); + + auto shutdownStorageOp = + CreateShutdownStorageOp(WrapMovingNotNullUnchecked(this)); + + RegisterNormalOriginOp(*shutdownStorageOp); + + shutdownStorageOp->RunImmediately(); + + mShutdownStorageOpCount++; + + return shutdownStorageOp->OnResults()->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue& aValue) { + self->mShutdownStorageOpCount--; + + if (aValue.IsReject()) { + return BoolPromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + self->mTemporaryStorageInitialized = false; + self->mStorageInitialized = false; + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +void QuotaManager::ShutdownStorageInternal() { + AssertIsOnIOThread(); + + if (mStorageConnection) { + mInitializationInfo.ResetOriginInitializationInfos(); + mInitializedOrigins.Clear(); + + if (mTemporaryStorageInitializedInternal) { + if (mCacheUsable) { + UnloadQuota(); + } else { + RemoveQuota(); + } + + mTemporaryStorageInitializedInternal = 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(mTemporaryStorageInitializedInternal); + + 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 (IsUUIDOrigin(origin)) { + 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. + + nsCString spec; + OriginAttributes attrs; + nsCString originalSuffix; + const OriginParser::ResultType result = OriginParser::ParseOrigin( + MakeSanitizedOriginCString(aOrigin), 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))); + + return std::move(principalInfo); +} + +// 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) const { + 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!"); + + auto finalizeOriginEviction = [locks = std::move(aLocks)]() mutable { + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + RefPtr<OriginOperationBase> op = CreateFinalizeOriginEvictionOp( + WrapMovingNotNull(quotaManager), std::move(locks)); + + op->RunImmediately(); + }; + + if (IsOnBackgroundThread()) { + finalizeOriginEviction(); + } else { + MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch( + NS_NewRunnableFunction( + "dom::quota::QuotaManager::FinalizeOriginEviction", + std::move(finalizeOriginEviction)), + NS_DISPATCH_NORMAL)); + } +} + +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!"); + } +} + +void QuotaManager::ClearDirectoryLockTables() { + AssertIsOnOwningThread(); + + for (const PersistenceType type : kBestEffortPersistenceTypes) { + DirectoryLockTable& directoryLockTable = GetDirectoryLockTable(type); + + if (!IsShuttingDown()) { + for (const auto& entry : directoryLockTable) { + const auto& array = entry.GetData(); + + // It doesn't matter which lock is used, they all have the same + // persistence type and origin metadata. + MOZ_ASSERT(!array->IsEmpty()); + const auto& lock = array->ElementAt(0); + + UpdateOriginAccessTime(lock->GetPersistenceType(), + lock->OriginMetadata()); + } + } + + directoryLockTable.Clear(); + } +} + +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; +} + +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 RecordTimeDeltaHelper::Start() { + MOZ_ASSERT(IsOnIOThread() || IsOnBackgroundThread()); + + // 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 RecordTimeDeltaHelper::End() { + MOZ_ASSERT(IsOnIOThread() || IsOnBackgroundThread()); + + mEndTime.init(TimeStamp::Now()); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this)); + + return *mEndTime; +} + +NS_IMETHODIMP +RecordTimeDeltaHelper::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mInitializedTime.isSome()) { + // Keys for QM_QUOTA_INFO_LOAD_TIME_V0 and QM_SHUTDOWN_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(mHistogram, key, *mStartTime, *mEndTime); + + return NS_OK; + } + + gLastOSWake = TimeStamp::Now(); + mInitializedTime.init(gLastOSWake); + + return NS_OK; +} + +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(QuotaManager::CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + + QM_TRY(MOZ_TO_RESULT(QuotaManager::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)); + + // XXX Consider using QuotaManager::ParseOrigin here. + 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; +} + +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(QuotaManager::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(QuotaManager::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(QuotaManager::CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + QM_TRY(MOZ_TO_RESULT(QuotaManager::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(QuotaManager::CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(QuotaManager::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(QuotaManager::CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(QuotaManager::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(QuotaManager::CreateDirectoryMetadata( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + aOriginProps.mOriginMetadata))); + } + + if (aOriginProps.mNeedsRestore2) { + QM_TRY(MOZ_TO_RESULT(QuotaManager::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(QuotaManager::CreateDirectoryMetadata2( + *aOriginProps.mDirectory, aOriginProps.mTimestamp, + /* aPersisted */ false, aOriginProps.mOriginMetadata))); + + return NS_OK; +} + +} // namespace mozilla::dom::quota |